@icen.ai/ui 0.2.1 → 0.3.0

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.
Files changed (103) hide show
  1. package/README.md +29 -16
  2. package/dist/base.css +389 -48
  3. package/dist/behaviors/accordion.mjs +20 -0
  4. package/dist/behaviors/carousel.mjs +30 -1
  5. package/dist/behaviors/charts.mjs +168 -0
  6. package/dist/behaviors/copy.mjs +4 -3
  7. package/dist/behaviors/datatable.mjs +748 -0
  8. package/dist/behaviors/input.mjs +20 -1
  9. package/dist/behaviors/modal.mjs +14 -0
  10. package/dist/behaviors/nav.mjs +31 -4
  11. package/dist/behaviors/sidebar.mjs +12 -0
  12. package/dist/behaviors/slider.mjs +35 -3
  13. package/dist/behaviors/tabs.mjs +100 -9
  14. package/dist/behaviors/tag-input.mjs +11 -0
  15. package/dist/behaviors/theme.mjs +26 -0
  16. package/dist/behaviors/toast.mjs +83 -15
  17. package/dist/behaviors/tree.mjs +57 -1
  18. package/dist/behaviors/upload.mjs +2 -3
  19. package/dist/cli.mjs +74 -0
  20. package/dist/components/accordion.mjs +3 -0
  21. package/dist/components/alert.mjs +2 -0
  22. package/dist/components/avatar.mjs +2 -0
  23. package/dist/components/breadcrumb.css +91 -18
  24. package/dist/components/breadcrumb.mjs +2 -0
  25. package/dist/components/btn.css +174 -14
  26. package/dist/components/btn.mjs +2 -0
  27. package/dist/components/card.css +118 -18
  28. package/dist/components/card.mjs +2 -0
  29. package/dist/components/carousel.mjs +3 -0
  30. package/dist/components/charts.css +225 -33
  31. package/dist/components/charts.mjs +3 -0
  32. package/dist/components/content.css +256 -74
  33. package/dist/components/context-menu.mjs +3 -0
  34. package/dist/components/datatable.css +343 -0
  35. package/dist/components/datatable.mjs +4 -0
  36. package/dist/components/desc.mjs +2 -0
  37. package/dist/components/dropdown.mjs +3 -0
  38. package/dist/components/empty.css +50 -10
  39. package/dist/components/empty.mjs +2 -0
  40. package/dist/components/feedback.css +158 -47
  41. package/dist/components/form.css +139 -22
  42. package/dist/components/input.css +165 -45
  43. package/dist/components/input.mjs +3 -0
  44. package/dist/components/kbd.mjs +2 -0
  45. package/dist/components/layout.mjs +2 -0
  46. package/dist/components/list.mjs +2 -0
  47. package/dist/components/media-card.mjs +2 -0
  48. package/dist/components/media.css +187 -40
  49. package/dist/components/menu.css +159 -32
  50. package/dist/components/modal.css +140 -34
  51. package/dist/components/modal.mjs +3 -0
  52. package/dist/components/nav.css +134 -32
  53. package/dist/components/nav.mjs +3 -0
  54. package/dist/components/pagination.css +105 -19
  55. package/dist/components/pagination.mjs +2 -0
  56. package/dist/components/panel.css +61 -6
  57. package/dist/components/panel.mjs +2 -0
  58. package/dist/components/pill.css +74 -14
  59. package/dist/components/pill.mjs +2 -0
  60. package/dist/components/popover.css +109 -7
  61. package/dist/components/popover.mjs +3 -0
  62. package/dist/components/progress.mjs +2 -0
  63. package/dist/components/rating.mjs +2 -0
  64. package/dist/components/result.mjs +2 -0
  65. package/dist/components/segmented.css +199 -50
  66. package/dist/components/segmented.mjs +2 -0
  67. package/dist/components/select.css +140 -39
  68. package/dist/components/select.mjs +3 -0
  69. package/dist/components/sidebar.css +162 -34
  70. package/dist/components/sidebar.mjs +3 -0
  71. package/dist/components/skeleton.mjs +2 -0
  72. package/dist/components/slider.css +169 -31
  73. package/dist/components/slider.mjs +3 -0
  74. package/dist/components/spinner.mjs +2 -0
  75. package/dist/components/stat.css +103 -8
  76. package/dist/components/stat.mjs +2 -0
  77. package/dist/components/steps.css +105 -31
  78. package/dist/components/steps.mjs +2 -0
  79. package/dist/components/switch.css +171 -54
  80. package/dist/components/switch.mjs +2 -0
  81. package/dist/components/table.css +114 -12
  82. package/dist/components/table.mjs +2 -0
  83. package/dist/components/tabs.css +104 -17
  84. package/dist/components/tabs.mjs +3 -0
  85. package/dist/components/tag-input.css +119 -23
  86. package/dist/components/tag-input.mjs +3 -0
  87. package/dist/components/timeline.mjs +2 -0
  88. package/dist/components/toast.css +85 -12
  89. package/dist/components/toast.mjs +3 -0
  90. package/dist/components/tooltip.css +86 -26
  91. package/dist/components/tooltip.mjs +2 -0
  92. package/dist/components/tree.mjs +3 -0
  93. package/dist/components/upload.css +144 -17
  94. package/dist/components/upload.mjs +3 -0
  95. package/dist/index.mjs +623 -42
  96. package/dist/registry.json +197 -2
  97. package/dist/retro-effects.css +264 -0
  98. package/dist/tokens/colors.css +18 -90
  99. package/dist/tokens/retro-effects.css +264 -0
  100. package/dist/tokens/style-profiles.css +47 -47
  101. package/dist/tokens.css +65 -137
  102. package/dist/ui.css +4861 -1019
  103. package/package.json +9 -1
@@ -2,81 +2,201 @@
2
2
  * @icen.ai/ui — Component: input(wrapper 式输入框 / 多行 / 搜索)
3
3
  * 规格源自 opengal primitives/input.tsx;与 behaviors/input.ts 配套。
4
4
  *
5
- * 契约:
6
- * <div class="input-wrap control [--sm|--lg] [.is-error] [.is-disabled]">
7
- * <span class="input-leading">…</span> 可选前缀
5
+ * 完整契约:
6
+ * <div class="input-wrap control [--sm|--lg] [--search] [.is-error] [.is-disabled]">
7
+ * <span class="input-leading">…</span> 可选前缀(svg/icon/text)
8
8
  * <input class="input" />
9
- * <button class="input-clear">…</button> 可选清除钮(behavior 清空并聚焦)
10
- * <button class="input-pw-toggle">…</button> 可选密码可见切换(behavior 驱动)
11
- * <span class="input-trailing">…</span> 可选后缀
9
+ * <button class="input-clear" aria-label="清除">…</button> behavior 清空并聚焦
10
+ * <button class="input-pw-toggle" aria-label="切换可见性">…</button>
11
+ * <span class="input-trailing">…</span> 可选后缀(单位/字符计数等)
12
12
  * </div>
13
- * 多行:<div class="textarea-wrap control"><textarea class="textarea" data-autosize></textarea></div>
14
- * 搜索:.input-wrap--search = leading 放大镜 + .input-clear 的结构约定(无额外视觉)
13
+ *
14
+ * 多行:<div class="textarea-wrap control" [.is-error|.is-disabled]><textarea class="textarea" data-autosize></textarea></div>
15
+ *
16
+ * --search 修饰:仅语义标记(让 behavior 知道这是搜索框,绑定 clear + Esc),
17
+ * 不额外改样式;视觉上仍用 .input-leading 放大镜。
15
18
  */
16
19
 
20
+ /* ── 单行 ── */
17
21
  .input-wrap {
18
- display: flex; align-items: center; gap: 8px; width: 100%;
19
- height: 36px; padding: 0 12px; font-size: 13px;
22
+ display: flex;
23
+ align-items: center;
24
+ gap: var(--density-space-xs);
25
+ width: 100%;
26
+ height: var(--density-input-height-md);
27
+ padding: 0 var(--density-space-sm);
28
+ font-size: var(--density-font-size-md);
20
29
  background: var(--token-control);
21
30
  border: 1px solid var(--token-outline-soft);
22
- border-radius: var(--radius-lg);
31
+ border-radius: var(--radius-md);
23
32
  box-shadow: var(--token-shadow-inset);
33
+ transition: border-color var(--icen-tx-fast),
34
+ box-shadow var(--icen-tx-fast);
35
+ }
36
+ .input-wrap:hover {
37
+ border-color: var(--icen-hover-border);
38
+ }
39
+ .input-wrap:focus-within {
40
+ box-shadow: var(--icen-control-focus-shadow);
41
+ border-color: var(--icen-control-focus-border);
42
+ }
43
+ .input-wrap.is-error,
44
+ .input-wrap.is-error:focus-within {
45
+ border-color: var(--token-error);
46
+ }
47
+ .input-wrap.is-error:focus-within {
48
+ box-shadow: var(--icen-control-error-shadow);
49
+ }
50
+ .input-wrap.is-disabled,
51
+ .input-wrap[aria-disabled='true'],
52
+ .input-wrap[data-disabled] {
53
+ opacity: var(--icen-disabled-opacity);
54
+ background: var(--token-bg-soft);
55
+ cursor: not-allowed;
56
+ pointer-events: none;
24
57
  }
25
- .input-wrap:focus-within { border-color: var(--token-accent); }
26
- .input-wrap.is-error, .input-wrap.is-error:focus-within { border-color: var(--token-error); }
27
- .input-wrap.is-disabled { opacity: 0.4; }
28
58
  .input-wrap.is-disabled .input { cursor: not-allowed; }
29
59
 
30
- /* 尺寸三档:高度 / 横向内边距 / 字号 */
31
- .input-wrap--sm { height: 32px; padding: 0 10px; font-size: 12px; }
32
- .input-wrap--lg { height: 40px; padding: 0 14px; font-size: 14px; }
60
+ /* 尺寸变体 */
61
+ .input-wrap--sm {
62
+ height: var(--density-input-height-sm);
63
+ padding: 0 10px;
64
+ font-size: var(--density-font-size-sm);
65
+ }
66
+ .input-wrap--lg {
67
+ height: var(--density-input-height-lg);
68
+ padding: 0 var(--density-space-md);
69
+ font-size: var(--density-font-size-lg);
70
+ }
33
71
 
34
72
  .input {
35
- flex: 1; min-width: 0; padding: 0;
36
- background: transparent; border: none; outline: none;
37
- color: var(--token-text); font: inherit;
73
+ flex: 1;
74
+ min-width: 0;
75
+ padding: 0;
76
+ background: transparent;
77
+ border: none;
78
+ outline: none;
79
+ color: var(--token-text);
80
+ font: inherit;
81
+ }
82
+ .input::placeholder {
83
+ color: color-mix(in srgb, var(--token-text-faint) 60%, transparent);
84
+ }
85
+ .input:read-only {
86
+ color: var(--token-text-muted);
87
+ cursor: default;
38
88
  }
39
- .input::placeholder { color: color-mix(in srgb, var(--token-text-faint) 60%, transparent); }
89
+ .input::-webkit-search-cancel-button { -webkit-appearance: none; }
40
90
 
41
- .input-leading, .input-trailing {
42
- display: inline-flex; align-items: center; flex-shrink: 0;
91
+ .input-leading,
92
+ .input-trailing {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ flex-shrink: 0;
43
96
  color: var(--token-text-faint);
44
97
  }
98
+ .input-leading svg, .input-trailing svg { width: 14px; height: 14px; }
45
99
 
46
- .input-clear {
47
- display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
48
- padding: 2px; border: none; background: transparent; cursor: pointer;
49
- border-radius: var(--radius-full); color: var(--token-text-faint);
50
- transition: color var(--duration-normal) var(--ease-out),
51
- background-color var(--duration-normal) var(--ease-out);
52
- }
53
- .input-clear:hover { color: var(--token-text); background: var(--token-surface-hover); }
100
+ /* ── 前缀/后缀文字(单位、域名等)── */
101
+ .input-affix {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ flex-shrink: 0;
105
+ padding: 0 var(--density-space-xs);
106
+ color: var(--token-text-muted);
107
+ font-size: 0.92em;
108
+ background: var(--token-fill-1);
109
+ border-radius: var(--radius-xs);
110
+ }
54
111
 
112
+ .input-clear,
55
113
  .input-pw-toggle {
56
- display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
57
- padding: 2px; border: none; background: transparent; cursor: pointer;
114
+ display: inline-flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ flex-shrink: 0;
118
+ width: 22px; height: 22px;
119
+ padding: 0;
120
+ border: none;
121
+ background: transparent;
122
+ cursor: pointer;
123
+ border-radius: var(--radius-full);
58
124
  color: var(--token-text-faint);
59
- transition: color var(--duration-normal) var(--ease-out);
125
+ transition:
126
+ color var(--icen-tx-fast),
127
+ background-color var(--icen-tx-fast);
128
+ }
129
+ .input-clear:hover,
130
+ .input-pw-toggle:hover {
131
+ color: var(--token-text);
132
+ background: var(--token-surface-hover);
133
+ }
134
+ .input-clear:focus-visible,
135
+ .input-pw-toggle:focus-visible {
136
+ outline: none;
137
+ box-shadow: var(--icen-ring);
60
138
  }
61
- .input-pw-toggle:hover { color: var(--token-text); }
139
+ .input-clear svg, .input-pw-toggle svg { width: 12px; height: 12px; }
62
140
 
63
141
  /* ── 多行:同一 wrapper 视觉,内部 textarea 裸奔 ── */
64
142
  .textarea-wrap {
65
- display: flex; width: 100%;
143
+ display: flex;
144
+ width: 100%;
66
145
  background: var(--token-control);
67
146
  border: 1px solid var(--token-outline-soft);
68
- border-radius: var(--radius-lg);
147
+ border-radius: var(--radius-md);
69
148
  box-shadow: var(--token-shadow-inset);
149
+ transition: border-color var(--icen-tx-fast),
150
+ box-shadow var(--icen-tx-fast);
151
+ }
152
+ .textarea-wrap:hover {
153
+ border-color: var(--icen-hover-border);
154
+ }
155
+ .textarea-wrap:focus-within {
156
+ box-shadow: var(--icen-control-focus-shadow);
157
+ border-color: var(--icen-control-focus-border);
158
+ }
159
+ .textarea-wrap.is-error,
160
+ .textarea-wrap.is-error:focus-within {
161
+ border-color: var(--token-error);
162
+ box-shadow: var(--icen-control-error-shadow);
163
+ }
164
+ .textarea-wrap.is-disabled,
165
+ .textarea-wrap[aria-disabled='true'],
166
+ .textarea-wrap[data-disabled] {
167
+ opacity: var(--icen-disabled-opacity);
168
+ background: var(--token-bg-soft);
169
+ cursor: not-allowed;
170
+ pointer-events: none;
70
171
  }
71
- .textarea-wrap:focus-within { border-color: var(--token-accent); }
72
- .textarea-wrap.is-error, .textarea-wrap.is-error:focus-within { border-color: var(--token-error); }
73
- .textarea-wrap.is-disabled { opacity: 0.4; }
74
172
  .textarea-wrap.is-disabled .textarea { cursor: not-allowed; }
75
173
 
76
174
  .textarea {
77
- width: 100%; padding: 12px;
78
- background: transparent; border: none; outline: none; resize: none;
79
- color: var(--token-text); font: inherit; font-size: 13px; line-height: 1.5;
80
- min-height: calc(3 * 1.5em + 24px); /* 3 行 + 上下 padding */
175
+ width: 100%;
176
+ padding: var(--density-space-sm) var(--density-space-md);
177
+ background: transparent;
178
+ border: none;
179
+ outline: none;
180
+ resize: vertical;
181
+ color: var(--token-text);
182
+ font: inherit;
183
+ font-size: var(--density-font-size-md);
184
+ line-height: 1.5;
185
+ min-height: calc(3 * 1.5em + 24px);
186
+ }
187
+ .textarea::placeholder {
188
+ color: color-mix(in srgb, var(--token-text-faint) 60%, transparent);
189
+ }
190
+
191
+ /* 字符计数(behavior 可挂到 .textarea-wrap 内尾部) */
192
+ .input-counter {
193
+ position: absolute;
194
+ right: var(--density-space-sm);
195
+ bottom: var(--density-space-xs);
196
+ font-size: var(--density-font-size-xs);
197
+ color: var(--token-text-faint);
198
+ font-variant-numeric: tabular-nums;
199
+ pointer-events: none;
81
200
  }
82
- .textarea::placeholder { color: color-mix(in srgb, var(--token-text-faint) 60%, transparent); }
201
+ .textarea-wrap { position: relative; }
202
+ .textarea-wrap .textarea { padding-bottom: 24px; }
@@ -0,0 +1,3 @@
1
+ // @icen.ai/ui — kit: input(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './input.css';
3
+ export * from '../behaviors/input.mjs';
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: kbd(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './content.css';
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: layout(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './layout.css';
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: list(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './content.css';
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: media-card(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './media.css';
@@ -1,37 +1,79 @@
1
1
  /*
2
2
  * @icen.ai/ui — Component: media(媒体族)
3
+ *
3
4
  * 包含:avatar(头像)/ media-card(封面卡,滑出 overlay)/ rating(星级评分)/ carousel(轮播,与 behaviors/carousel 配套)
4
- * carousel 归入本文件(无独立 carousel.css 配额);规格源自 opengal data/display.tsx 的 Avatar、
5
- * data/cartridge-card.tsx 的 hover overlay、data/exhibit.tsx 的 Carousel 与 Rating。
5
+ * 规格源自 opengal data/display.tsx 的 Avatar、data/cartridge-card.tsx 的 hover overlay、
6
+ * data/exhibit.tsx 的 Carousel 与 Rating。
6
7
  */
7
8
 
8
9
  /* ══════════════ avatar · 头像 ══════════════ */
9
10
  .avatar {
10
11
  position: relative;
11
- display: inline-flex; align-items: center; justify-content: center;
12
+ display: inline-flex;
13
+ align-items: center;
14
+ justify-content: center;
12
15
  overflow: hidden;
13
16
  border-radius: var(--radius-full);
14
17
  background: var(--token-accent-subtle);
15
18
  color: var(--token-accent);
16
19
  font-weight: 600;
17
- width: 36px; height: 36px; font-size: 12px;
20
+ width: 36px;
21
+ height: 36px;
22
+ font-size: var(--density-font-size-sm);
18
23
  flex-shrink: 0;
24
+ user-select: none;
19
25
  }
20
26
  .avatar--xs { width: 20px; height: 20px; font-size: 9px; }
21
27
  .avatar--sm { width: 28px; height: 28px; font-size: 10px; }
22
- .avatar--lg { width: 48px; height: 48px; font-size: 14px; }
23
- .avatar img { width: 100%; height: 100%; object-fit: cover; }
28
+ .avatar--lg { width: 48px; height: 48px; font-size: var(--density-font-size-md); }
29
+ .avatar--xl { width: 80px; height: 80px; font-size: var(--density-font-size-lg); }
30
+ .avatar img {
31
+ width: 100%;
32
+ height: 100%;
33
+ object-fit: cover;
34
+ }
35
+
36
+ /* 头像组:多个头像重叠 */
37
+ .avatar-group {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ }
41
+ .avatar-group .avatar {
42
+ border: 2px solid var(--token-bg);
43
+ margin-left: -8px;
44
+ }
45
+ .avatar-group .avatar:first-child { margin-left: 0; }
46
+
47
+ /* 在线状态点 */
48
+ .avatar-status {
49
+ position: absolute;
50
+ bottom: 0;
51
+ right: 0;
52
+ width: 10px;
53
+ height: 10px;
54
+ border: 2px solid var(--token-bg);
55
+ border-radius: var(--radius-full);
56
+ background: var(--token-text-faint);
57
+ }
58
+ .avatar-status--online { background: var(--token-success); }
59
+ .avatar-status--busy { background: var(--token-error); }
60
+ .avatar-status--away { background: var(--token-warning); }
24
61
 
25
62
  /* ══════════════ media-card · 封面卡 ══════════════ */
26
63
  .media-card {
27
64
  position: relative;
28
65
  overflow: hidden;
29
66
  border-radius: var(--radius-lg);
30
- transition: transform 300ms var(--ease-out), box-shadow 300ms var(--ease-out);
67
+ background: var(--token-bg-soft);
68
+ transition:
69
+ transform var(--icen-tx-slow),
70
+ box-shadow var(--icen-tx-slow);
31
71
  }
32
72
  .media-card:hover {
33
73
  transform: translateY(-6px);
34
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2), 0 0 20px var(--token-accent-glow);
74
+ box-shadow:
75
+ 0 8px 32px color-mix(in srgb, var(--token-text) 18%, transparent),
76
+ 0 0 20px var(--token-accent-glow);
35
77
  }
36
78
  .media-card-cover {
37
79
  display: block;
@@ -40,52 +82,95 @@
40
82
  object-fit: cover;
41
83
  }
42
84
  .media-card-overlay {
43
- position: absolute; left: 0; right: 0; bottom: 0;
44
- padding: 32px 12px 10px;
45
- background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.35) 65%, transparent);
85
+ position: absolute;
86
+ left: 0; right: 0; bottom: 0;
87
+ padding: 32px var(--density-space-sm) var(--density-space-xs);
88
+ background: linear-gradient(
89
+ to top,
90
+ color-mix(in srgb, var(--token-text) 72%, transparent),
91
+ color-mix(in srgb, var(--token-text) 35%, transparent) 65%,
92
+ transparent
93
+ );
46
94
  backdrop-filter: blur(10px);
47
95
  -webkit-backdrop-filter: blur(10px);
48
- color: #fff;
96
+ color: var(--token-bg);
49
97
  transform: translateY(100%);
50
- transition: transform 200ms var(--ease-out);
98
+ transition: transform var(--icen-tx-normal);
51
99
  pointer-events: none;
52
100
  }
53
101
  .media-card:hover .media-card-overlay {
54
102
  transform: translateY(0);
55
103
  transition-delay: 80ms;
56
104
  }
105
+ .media-card-overlay-title {
106
+ font-size: var(--density-font-size-md);
107
+ font-weight: 600;
108
+ margin: 0;
109
+ }
110
+ .media-card-overlay-desc {
111
+ font-size: var(--density-font-size-xs);
112
+ margin: 2px 0 0;
113
+ opacity: 0.8;
114
+ }
57
115
 
58
116
  /* ══════════════ rating · 星级评分 ══════════════ */
59
- .rating { display: inline-flex; align-items: center; gap: 2px; }
117
+ .rating {
118
+ display: inline-flex;
119
+ align-items: center;
120
+ gap: 2px;
121
+ }
60
122
  .rating-star {
61
123
  padding: 2px;
62
124
  border-radius: var(--radius-xs);
63
- background: none; border: 0;
125
+ background: none;
126
+ border: 0;
64
127
  cursor: pointer;
65
- display: inline-flex; align-items: center; justify-content: center;
128
+ display: inline-flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ -webkit-tap-highlight-color: transparent;
132
+ transition: transform var(--icen-tx-fast);
133
+ }
134
+ .rating-star:hover { transform: scale(1.1); }
135
+ .rating-star:focus-visible {
136
+ outline: none;
137
+ box-shadow: var(--icen-ring);
66
138
  }
67
139
  .rating-star svg {
68
- width: 18px; height: 18px; display: block;
140
+ width: 18px;
141
+ height: 18px;
142
+ display: block;
69
143
  fill: transparent;
70
144
  stroke: var(--token-line-strong);
71
145
  stroke-width: 1.6;
72
- transition: fill var(--duration-normal) var(--ease-out), stroke var(--duration-normal) var(--ease-out);
146
+ transition:
147
+ fill var(--icen-tx-fast),
148
+ stroke var(--icen-tx-fast);
73
149
  }
74
150
  .rating--sm .rating-star svg { width: 14px; height: 14px; }
75
151
  .rating--lg .rating-star svg { width: 22px; height: 22px; }
76
- .rating-star.is-filled svg { fill: var(--token-warning); stroke: var(--token-warning); }
152
+ .rating-star.is-filled svg {
153
+ fill: var(--token-warning);
154
+ stroke: var(--token-warning);
155
+ }
77
156
  .rating-star.is-half svg {
78
157
  fill: color-mix(in srgb, var(--token-warning) 50%, transparent);
79
158
  stroke: var(--token-warning);
80
159
  }
160
+ .rating-value {
161
+ margin-left: var(--density-space-xs);
162
+ font-size: var(--density-font-size-sm);
163
+ color: var(--token-text-muted);
164
+ font-variant-numeric: tabular-nums;
165
+ }
81
166
 
82
167
  /* ══════════════ carousel · 轮播(behaviors/carousel 配套) ══════════════
83
168
  * DOM 契约:
84
- * <div class="carousel">
169
+ * <div class="carousel" data-carousel>
85
170
  * <div class="carousel-track"><div class="carousel-slide">…</div>×n</div>
86
- * <button class="carousel-arrow carousel-arrow--prev">…</button>
87
- * <button class="carousel-arrow carousel-arrow--next">…</button>
88
- * <div class="carousel-dots"></div> <!-- dots 由 behavior 自动生成 -->
171
+ * <button class="carousel-arrow carousel-arrow--prev" aria-label="上一张">…</button>
172
+ * <button class="carousel-arrow carousel-arrow--next" aria-label="下一张">…</button>
173
+ * <div class="carousel-dots"></div> <!-- behavior 自动生成 .carousel-dot -->
89
174
  * </div>
90
175
  */
91
176
  .carousel {
@@ -96,38 +181,100 @@
96
181
  }
97
182
  .carousel-track {
98
183
  display: flex;
99
- transition: transform 300ms var(--ease-out);
184
+ transition: transform var(--icen-tx-slow);
185
+ }
186
+ .carousel-slide {
187
+ width: 100%;
188
+ flex-shrink: 0;
100
189
  }
101
- .carousel-slide { width: 100%; flex-shrink: 0; }
102
190
  .carousel-arrow {
103
- position: absolute; top: 50%; transform: translateY(-50%);
104
- width: 32px; height: 32px;
191
+ position: absolute;
192
+ top: 50%;
193
+ transform: translateY(-50%);
194
+ width: 32px;
195
+ height: 32px;
105
196
  border-radius: var(--radius-full);
106
197
  background: color-mix(in srgb, var(--token-card) 78%, transparent);
107
198
  border: 1px solid var(--token-outline-soft);
108
199
  color: var(--token-text);
109
- display: flex; align-items: center; justify-content: center;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
110
203
  backdrop-filter: blur(4px);
111
204
  -webkit-backdrop-filter: blur(4px);
112
205
  box-shadow: var(--token-shadow-card);
113
206
  cursor: pointer;
114
- transition: background-color var(--duration-normal) var(--ease-out), transform var(--duration-normal) var(--ease-out);
207
+ z-index: 2;
208
+ transition:
209
+ background-color var(--icen-tx-fast),
210
+ transform var(--icen-tx-fast),
211
+ opacity var(--icen-tx-fast);
212
+ }
213
+ .carousel-arrow:hover {
214
+ background: var(--token-card-strong);
215
+ }
216
+ .carousel-arrow:focus-visible {
217
+ outline: none;
218
+ box-shadow: var(--icen-ring), var(--token-shadow-card);
219
+ }
220
+ .carousel-arrow:active {
221
+ transform: translateY(-50%) scale(0.95);
222
+ }
223
+ .carousel-arrow:disabled,
224
+ .carousel-arrow[aria-disabled='true'] {
225
+ opacity: var(--icen-disabled-opacity);
226
+ cursor: not-allowed;
227
+ pointer-events: none;
115
228
  }
116
- .carousel-arrow:hover { background: var(--token-card-strong); }
117
- .carousel-arrow:active { transform: translateY(-50%) scale(0.95); }
118
229
  .carousel-arrow svg { width: 16px; height: 16px; }
119
- .carousel-arrow--prev { left: 8px; }
120
- .carousel-arrow--next { right: 8px; }
230
+ .carousel-arrow--prev { left: var(--density-space-sm); }
231
+ .carousel-arrow--next { right: var(--density-space-sm); }
232
+
121
233
  .carousel-dots {
122
- position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
123
- display: flex; gap: 6px;
234
+ position: absolute;
235
+ bottom: var(--density-space-sm);
236
+ left: 50%;
237
+ transform: translateX(-50%);
238
+ display: flex;
239
+ gap: 6px;
240
+ z-index: 2;
124
241
  }
125
242
  .carousel-dot {
126
- width: 6px; height: 6px; padding: 0;
127
- border: 0; border-radius: var(--radius-full);
243
+ width: 6px;
244
+ height: 6px;
245
+ padding: 0;
246
+ border: 0;
247
+ border-radius: var(--radius-full);
128
248
  background: color-mix(in srgb, var(--token-card) 70%, transparent);
129
249
  cursor: pointer;
130
- transition: width var(--duration-normal) var(--ease-out), background-color var(--duration-normal) var(--ease-out);
250
+ transition:
251
+ width var(--icen-tx-fast),
252
+ background-color var(--icen-tx-fast);
253
+ }
254
+ .carousel-dot:hover {
255
+ background: color-mix(in srgb, var(--token-accent) 70%, transparent);
256
+ }
257
+ .carousel-dot:focus-visible {
258
+ outline: none;
259
+ box-shadow: var(--icen-ring);
260
+ }
261
+ .carousel-dot.active,
262
+ .carousel-dot[aria-current='true'] {
263
+ width: 20px;
264
+ background: var(--token-accent);
265
+ }
266
+
267
+ /* 自动播放进度条(behavior 可选) */
268
+ .carousel-progress {
269
+ position: absolute;
270
+ bottom: 0;
271
+ left: 0;
272
+ height: 2px;
273
+ background: var(--token-accent);
274
+ transition: width var(--icen-tx-slow);
275
+ }
276
+
277
+ @media (prefers-reduced-motion: reduce) {
278
+ .carousel-track { transition: none; }
279
+ .media-card, .rating-star, .carousel-arrow { transition: none; }
131
280
  }
132
- .carousel-dot:hover { background: color-mix(in srgb, var(--token-accent) 70%, transparent); }
133
- .carousel-dot.active { width: 20px; background: var(--token-accent); }