@icen.ai/ui 0.1.1 → 0.2.1

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 (45) hide show
  1. package/README.md +11 -0
  2. package/dist/base.css +72 -0
  3. package/dist/behaviors/accordion.mjs +34 -0
  4. package/dist/behaviors/carousel.mjs +50 -0
  5. package/dist/behaviors/charts.mjs +267 -0
  6. package/dist/behaviors/context-menu.mjs +223 -0
  7. package/dist/behaviors/dropdown.mjs +136 -0
  8. package/dist/behaviors/input.mjs +102 -0
  9. package/dist/behaviors/modal.mjs +107 -0
  10. package/dist/behaviors/nav.mjs +29 -0
  11. package/dist/behaviors/popover.mjs +164 -0
  12. package/dist/behaviors/select.mjs +132 -0
  13. package/dist/behaviors/sidebar.mjs +27 -0
  14. package/dist/behaviors/slider.mjs +72 -0
  15. package/dist/behaviors/tag-input.mjs +74 -0
  16. package/dist/behaviors/tree.mjs +44 -0
  17. package/dist/behaviors/upload.mjs +55 -0
  18. package/dist/components/breadcrumb.css +31 -0
  19. package/dist/components/card.css +56 -0
  20. package/dist/components/charts.css +95 -0
  21. package/dist/components/content.css +195 -0
  22. package/dist/components/empty.css +25 -0
  23. package/dist/components/feedback.css +106 -0
  24. package/dist/components/input.css +82 -0
  25. package/dist/components/layout.css +109 -0
  26. package/dist/components/media.css +133 -0
  27. package/dist/components/menu.css +85 -0
  28. package/dist/components/modal.css +107 -0
  29. package/dist/components/nav.css +59 -0
  30. package/dist/components/pagination.css +32 -0
  31. package/dist/components/popover.css +25 -0
  32. package/dist/components/segmented.css +112 -0
  33. package/dist/components/select.css +79 -0
  34. package/dist/components/sidebar.css +54 -0
  35. package/dist/components/slider.css +69 -0
  36. package/dist/components/steps.css +61 -0
  37. package/dist/components/switch.css +95 -0
  38. package/dist/components/tag-input.css +48 -0
  39. package/dist/components/toast.css +1 -1
  40. package/dist/components/tooltip.css +56 -0
  41. package/dist/components/upload.css +43 -0
  42. package/dist/index.mjs +1502 -1
  43. package/dist/registry.json +41 -3
  44. package/dist/ui.css +1899 -1
  45. package/package.json +1 -1
package/dist/ui.css CHANGED
@@ -1973,6 +1973,112 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
1973
1973
  .dim { color: var(--token-text-muted); }
1974
1974
  .faint { color: var(--token-text-faint); }
1975
1975
 
1976
+ /* ── z-index 标尺(全组件统一引用,浮层一律 portal 到 body) ── */
1977
+ :root {
1978
+ --z-base: 0;
1979
+ --z-raised: 10;
1980
+ --z-toast: 100;
1981
+ --z-chrome: 200;
1982
+ --z-dialog: 500;
1983
+ --z-popover: 620;
1984
+ --z-tooltip: 700;
1985
+ }
1986
+
1987
+ /* ── 交互基元(全组件地基,规格源自 opengal interaction-classes) ── */
1988
+
1989
+ /* 可按压:hover 顶部高光 + active 下陷缩放 */
1990
+ .pressable {
1991
+ position: relative; overflow: hidden; will-change: transform;
1992
+ transition: background-color var(--duration-normal) var(--ease-out),
1993
+ border-color var(--duration-normal) var(--ease-out),
1994
+ box-shadow var(--duration-normal) var(--ease-out),
1995
+ color var(--duration-normal) var(--ease-out),
1996
+ opacity var(--duration-normal) var(--ease-out),
1997
+ transform var(--duration-normal) var(--ease-out);
1998
+ }
1999
+ .pressable::after {
2000
+ content: ''; position: absolute; inset: 0; border-radius: inherit;
2001
+ opacity: 0; pointer-events: none;
2002
+ background: linear-gradient(180deg, color-mix(in srgb, var(--token-surface) 24%, transparent), transparent 46%);
2003
+ transition: opacity var(--duration-normal) var(--ease-out);
2004
+ }
2005
+ .pressable:hover::after, .pressable:focus-visible::after { opacity: 1; }
2006
+ .pressable:active { transform: translateY(1px) scale(0.985); }
2007
+ .pressable:disabled, .pressable[aria-disabled='true'], .pressable[data-disabled] {
2008
+ transform: none;
2009
+ }
2010
+ .pressable:disabled::after, .pressable[aria-disabled='true']::after, .pressable[data-disabled]::after { display: none; }
2011
+
2012
+ /* 控件聚焦光晕(wrapper 式表单控件用 :focus-within) */
2013
+ .control {
2014
+ transition: background-color var(--duration-normal) var(--ease-out),
2015
+ border-color var(--duration-normal) var(--ease-out),
2016
+ box-shadow var(--duration-normal) var(--ease-out);
2017
+ }
2018
+ .control:focus-within {
2019
+ box-shadow: var(--token-shadow-inset), 0 0 0 3px color-mix(in srgb, var(--token-accent) 12%, transparent);
2020
+ }
2021
+
2022
+ /* 悬停浮起(可交互卡片) */
2023
+ .lift {
2024
+ transition: border-color var(--duration-slow) var(--ease-out),
2025
+ box-shadow var(--duration-slow) var(--ease-out),
2026
+ transform var(--duration-slow) var(--ease-out);
2027
+ }
2028
+ .lift:hover {
2029
+ transform: translateY(-1px);
2030
+ border-color: color-mix(in srgb, var(--token-accent) 26%, var(--token-outline-soft));
2031
+ box-shadow: var(--token-shadow-card);
2032
+ }
2033
+
2034
+ /* 聚焦环(ring-2 + offset-1 等价) */
2035
+ .focus-ring:focus-visible {
2036
+ outline: none;
2037
+ box-shadow: 0 0 0 1px var(--token-bg), 0 0 0 3px color-mix(in srgb, var(--token-accent) 60%, transparent);
2038
+ }
2039
+
2040
+ /* 动效开关(reduced-motion 时全部关闭) */
2041
+ @media (prefers-reduced-motion: no-preference) {
2042
+ .motion { transition: all var(--duration-normal) var(--ease-out); }
2043
+ }
2044
+ @media (prefers-reduced-motion: reduce) {
2045
+ *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
2046
+ }
2047
+
2048
+
2049
+ /* ── components/breadcrumb.css ── */
2050
+ /*
2051
+ * @icen.ai/ui — Component: breadcrumb(面包屑)
2052
+ * 用法:<nav class="breadcrumb" aria-label="面包屑"><ol>
2053
+ * <li><a class="breadcrumb-link" href="…">首页</a><span class="breadcrumb-sep" aria-hidden="true"></span></li>
2054
+ * <li><span class="breadcrumb-current" aria-current="page">当前页</span></li>
2055
+ * </ol></nav>
2056
+ * 分隔符是 .breadcrumb-sep 的 ::after(›);超长折叠用 .breadcrumb-ellipsis。
2057
+ */
2058
+
2059
+ .breadcrumb { display: flex; align-items: center; font-size: 13px; }
2060
+ .breadcrumb ol { display: flex; align-items: center; gap: 6px; list-style: none; margin: 0; padding: 0; }
2061
+ .breadcrumb li { display: flex; align-items: center; gap: 6px; }
2062
+
2063
+ .breadcrumb-link {
2064
+ display: inline-flex; align-items: center; gap: 6px;
2065
+ color: var(--token-text-muted); text-decoration: none;
2066
+ transition: color var(--duration-fast) var(--ease-out);
2067
+ }
2068
+ .breadcrumb-link:hover { color: var(--token-accent); text-decoration: none; }
2069
+ .breadcrumb-link > svg { width: 13px; height: 13px; flex-shrink: 0; }
2070
+
2071
+ .breadcrumb-sep { color: var(--token-text-faint); font-size: 13px; line-height: 1; }
2072
+ .breadcrumb-sep::after { content: '›'; }
2073
+
2074
+ .breadcrumb-current {
2075
+ display: inline-flex; align-items: center; gap: 6px;
2076
+ color: var(--token-text); font-weight: 500;
2077
+ }
2078
+ .breadcrumb-current > svg { width: 13px; height: 13px; flex-shrink: 0; }
2079
+
2080
+ .breadcrumb-ellipsis { padding: 0 4px; color: var(--token-text-faint); }
2081
+
1976
2082
 
1977
2083
  /* ── components/btn.css ── */
1978
2084
  /*
@@ -2000,6 +2106,498 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
2000
2106
  .btn-sm { padding: 6px 14px; font-size: 12px; }
2001
2107
 
2002
2108
 
2109
+ /* ── components/card.css ── */
2110
+ /*
2111
+ * @icen.ai/ui — Component: card(组合卡片容器)
2112
+ * 包含:.card 四变体(默认 / elevated / outlined / glass)+ .card--interactive + .is-selected
2113
+ * 子结构:.card-head / .card-title / .card-desc / .card-body / .card-foot / .card-meta / .card-action
2114
+ * 规格源自 opengal src/presentation/ui/data/card.tsx。
2115
+ */
2116
+
2117
+ .card {
2118
+ background: var(--token-card);
2119
+ border: 1px solid var(--token-outline-soft);
2120
+ box-shadow: var(--token-shadow-inset);
2121
+ border-radius: var(--radius-lg);
2122
+ }
2123
+
2124
+ /* ── 变体 ── */
2125
+ .card--elevated { box-shadow: var(--token-shadow-card); }
2126
+ .card--outlined { border-color: var(--token-outline); box-shadow: none; }
2127
+ .card--glass {
2128
+ background: var(--token-surface-glass);
2129
+ backdrop-filter: blur(12px);
2130
+ -webkit-backdrop-filter: blur(12px);
2131
+ box-shadow: none;
2132
+ }
2133
+
2134
+ /* ── 修饰 ── */
2135
+ /* 可交互:cursor + .lift 基元同款效果(纯 CSS 无法组合类,规则在此复制一份) */
2136
+ .card--interactive {
2137
+ cursor: pointer;
2138
+ transition: border-color var(--duration-slow) var(--ease-out),
2139
+ box-shadow var(--duration-slow) var(--ease-out),
2140
+ transform var(--duration-slow) var(--ease-out);
2141
+ }
2142
+ .card--interactive:hover {
2143
+ transform: translateY(-1px);
2144
+ border-color: color-mix(in srgb, var(--token-accent) 26%, var(--token-outline-soft));
2145
+ box-shadow: var(--token-shadow-card);
2146
+ }
2147
+ .card.is-selected {
2148
+ box-shadow: 0 0 0 2px var(--token-accent), 0 0 0 4px var(--token-bg);
2149
+ }
2150
+
2151
+ /* ── 子结构 ── */
2152
+ .card-head {
2153
+ display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
2154
+ padding: 16px;
2155
+ }
2156
+ .card-title { font-size: 15px; font-weight: 600; color: var(--token-text); line-height: 1.25; margin: 0; }
2157
+ .card-desc { font-size: 12px; color: var(--token-text-muted); margin: 2px 0 0; }
2158
+ .card-body { padding: 0 16px 16px; font-size: 13px; color: var(--token-text-muted); }
2159
+ .card-foot {
2160
+ display: flex; align-items: center; gap: 8px;
2161
+ padding: 12px 16px;
2162
+ border-top: 1px solid var(--token-outline-soft);
2163
+ }
2164
+ .card-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--token-text-faint); }
2165
+ .card-action { margin-left: auto; }
2166
+
2167
+
2168
+ /* ── components/charts.css ── */
2169
+ /*
2170
+ * @icen.ai/ui — Component: charts(轻量图表,与 behaviors/charts 配套)
2171
+ * 包含:.chart 容器 / .chart-empty / .chart-legend(图例行)/ .chart-labels(轴标签行)/
2172
+ * vbar(垂直柱状)/ hbar(水平条形)/ stack(堆叠条)/ donut(环形)/ line(折线)
2173
+ * DOM 全部由 behaviors/charts.ts 以 createElement(NS) + textContent 构建;
2174
+ * 单色调通过容器或条上的 --chart-tone 自定义属性注入(默认 var(--token-accent))。
2175
+ * 规格源自 opengal src/presentation/ui/data/charts.tsx。
2176
+ */
2177
+
2178
+ .chart { width: 100%; }
2179
+ .chart-empty {
2180
+ height: 150px;
2181
+ display: flex; align-items: center; justify-content: center;
2182
+ font-size: 11px; color: var(--token-text-faint);
2183
+ }
2184
+
2185
+ /* ── 图例 / 轴标签(donut / stack / vbar / line 共用) ── */
2186
+ .chart-legend { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1; }
2187
+ .chart-legend-item { display: flex; align-items: center; gap: 8px; font-size: 11px; min-width: 0; }
2188
+ .chart-legend-dot {
2189
+ width: 8px; height: 8px; flex-shrink: 0;
2190
+ border-radius: var(--radius-full);
2191
+ background: var(--chart-tone, var(--token-accent));
2192
+ }
2193
+ .chart-legend-label {
2194
+ flex: 1; min-width: 0;
2195
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2196
+ color: var(--token-text-muted);
2197
+ }
2198
+ .chart-legend-value { flex-shrink: 0; font-variant-numeric: tabular-nums; color: var(--token-text-faint); }
2199
+
2200
+ .chart-labels { display: flex; gap: 8px; margin-top: 8px; }
2201
+ .chart-labels > span {
2202
+ flex: 1; min-width: 0;
2203
+ text-align: center; font-size: 9px; color: var(--token-text-faint);
2204
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2205
+ }
2206
+
2207
+ /* ── vbar · 垂直柱状 ── */
2208
+ .chart-vbar { display: flex; align-items: flex-end; gap: 8px; height: 112px; }
2209
+ .chart-vbar-track {
2210
+ flex: 1; height: 100%; min-width: 0;
2211
+ display: flex; align-items: flex-end;
2212
+ background: color-mix(in srgb, var(--token-fill-2) 45%, transparent);
2213
+ border-radius: var(--radius-sm);
2214
+ }
2215
+ .chart-vbar-bar {
2216
+ width: 100%;
2217
+ border-radius: var(--radius-sm);
2218
+ background: var(--chart-tone, var(--token-accent));
2219
+ transition: height 500ms var(--ease-out);
2220
+ }
2221
+
2222
+ /* ── hbar · 水平条形 ── */
2223
+ .chart-hbar { display: flex; flex-direction: column; gap: 8px; }
2224
+ .chart-hbar-row {
2225
+ display: grid;
2226
+ grid-template-columns: minmax(72px, 1fr) minmax(90px, 2fr) 52px;
2227
+ gap: 8px; align-items: center;
2228
+ font-size: 11px;
2229
+ }
2230
+ .chart-hbar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--token-text-muted); }
2231
+ .chart-hbar-track {
2232
+ height: 10px; overflow: hidden;
2233
+ border-radius: var(--radius-full);
2234
+ background: var(--token-fill-2);
2235
+ }
2236
+ .chart-hbar-bar {
2237
+ height: 100%;
2238
+ border-radius: var(--radius-full);
2239
+ background: var(--chart-tone, var(--token-accent));
2240
+ transition: width 500ms var(--ease-out);
2241
+ }
2242
+ .chart-hbar-value { text-align: right; font-variant-numeric: tabular-nums; color: var(--token-text-faint); }
2243
+
2244
+ /* ── stack · 堆叠条 ── */
2245
+ .chart-stack {
2246
+ height: 16px;
2247
+ display: flex; overflow: hidden;
2248
+ border-radius: var(--radius-full);
2249
+ background: var(--token-fill-2);
2250
+ margin-bottom: 12px;
2251
+ }
2252
+ .chart-stack-seg {
2253
+ height: 100%; min-width: 3px;
2254
+ background: var(--chart-tone, var(--token-accent));
2255
+ transition: width 500ms var(--ease-out);
2256
+ }
2257
+
2258
+ /* ── donut · 环形 ── */
2259
+ .chart-donut { display: flex; align-items: center; gap: 12px; min-height: 150px; }
2260
+ .chart-donut > svg { width: 132px; height: 132px; flex-shrink: 0; }
2261
+
2262
+ /* ── line · 折线 ── */
2263
+ .chart-line > svg { display: block; width: 100%; height: 150px; overflow: visible; }
2264
+
2265
+
2266
+ /* ── components/content.css ── */
2267
+ /*
2268
+ * @icen.ai/ui — Component: content(内容展示族)
2269
+ * 包含:kbd(按键)/ code-inline(行内代码)/ list(分隔列表)/ accordion(手风琴,behaviors/accordion 配套)/
2270
+ * timeline(时间线)/ desc(描述列表)/ tree(树形,behaviors/tree 配套)
2271
+ * 规格源自 opengal src/presentation/ui/data/exhibit.tsx 与 data/display.tsx。
2272
+ */
2273
+
2274
+ /* ══════════════ kbd · 按键 ══════════════ */
2275
+ .kbd {
2276
+ display: inline-flex; align-items: center; justify-content: center;
2277
+ vertical-align: middle;
2278
+ font-family: var(--font-mono); font-weight: 500;
2279
+ border-radius: var(--radius-xs);
2280
+ border: 1px solid var(--token-outline);
2281
+ background: var(--token-control);
2282
+ color: var(--token-text-muted);
2283
+ box-shadow: var(--token-shadow-inset);
2284
+ height: 20px; min-width: 20px;
2285
+ padding: 0 6px;
2286
+ font-size: 11px;
2287
+ }
2288
+ .kbd--sm { height: 16px; min-width: 16px; padding: 0 4px; font-size: 10px; }
2289
+
2290
+ /* ══════════════ code-inline · 行内代码 ══════════════ */
2291
+ .code-inline {
2292
+ padding: 2px 6px;
2293
+ border-radius: var(--radius-xs);
2294
+ background: var(--token-control);
2295
+ border: 1px solid var(--token-outline-soft);
2296
+ font-size: 12px;
2297
+ font-family: var(--font-mono);
2298
+ color: var(--token-accent);
2299
+ }
2300
+
2301
+ /* ══════════════ list · 分隔列表 ══════════════ */
2302
+ .list { list-style: none; margin: 0; padding: 0; }
2303
+ .list-item {
2304
+ padding: 8px;
2305
+ border-radius: var(--radius-md);
2306
+ transition: background-color var(--duration-normal) var(--ease-out);
2307
+ }
2308
+ .list-item:hover { background: var(--token-control-hover); }
2309
+ .list .list-item + .list-item { border-top: 1px solid var(--token-outline-soft); }
2310
+
2311
+ /* ══════════════ accordion · 手风琴(behaviors/accordion 配套) ══════════════
2312
+ * DOM 契约:
2313
+ * <div class="accordion" data-single?>
2314
+ * <div class="accordion-item">
2315
+ * <button class="accordion-trigger">标题 <svg class="accordion-chevron"/></button>
2316
+ * <div class="accordion-panel"><div class="accordion-panel-inner">内容</div></div>
2317
+ * </div>
2318
+ * </div>
2319
+ * 展开 = item 挂 .is-open;panel 用 grid-template-rows 0fr→1fr 做过渡。
2320
+ */
2321
+ .accordion-item { border-bottom: 1px solid var(--token-outline-soft); }
2322
+ .accordion-item:last-child { border-bottom: 0; }
2323
+ .accordion-trigger {
2324
+ width: 100%;
2325
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
2326
+ padding: 12px 8px;
2327
+ font-size: 14px; font-weight: 500;
2328
+ color: var(--token-text); text-align: left;
2329
+ background: none; border: 0; cursor: pointer;
2330
+ border-radius: var(--radius-md);
2331
+ transition: background-color var(--duration-normal) var(--ease-out);
2332
+ }
2333
+ .accordion-trigger:hover { background: var(--token-control-hover); }
2334
+ .accordion-chevron {
2335
+ width: 14px; height: 14px; flex-shrink: 0;
2336
+ color: var(--token-text-faint);
2337
+ transition: transform var(--duration-normal) var(--ease-out);
2338
+ }
2339
+ .accordion-item.is-open .accordion-chevron { transform: rotate(180deg); }
2340
+ .accordion-panel {
2341
+ display: grid;
2342
+ grid-template-rows: 0fr;
2343
+ transition: grid-template-rows var(--duration-normal) var(--ease-out);
2344
+ }
2345
+ .accordion-item.is-open .accordion-panel { grid-template-rows: 1fr; }
2346
+ .accordion-panel-inner {
2347
+ overflow: hidden;
2348
+ padding: 0 8px 12px;
2349
+ font-size: 13px;
2350
+ color: var(--token-text-muted);
2351
+ line-height: 1.6;
2352
+ }
2353
+
2354
+ /* ══════════════ timeline · 时间线 ══════════════
2355
+ * DOM 契约:.timeline-item > .timeline-dot + .timeline-body
2356
+ * 连接线 = item 的 ::after(相对 item 绝对定位,从 dot 下方贯穿到下一项),末项不画。
2357
+ */
2358
+ .timeline { display: flex; flex-direction: column; }
2359
+ .timeline-item {
2360
+ position: relative;
2361
+ display: flex; gap: 12px;
2362
+ padding-bottom: 24px;
2363
+ }
2364
+ .timeline-item:last-child { padding-bottom: 0; }
2365
+ .timeline-item:not(:last-child)::after {
2366
+ content: '';
2367
+ position: absolute;
2368
+ left: 13.5px; /* 28px dot 的水平中心 */
2369
+ top: 32px; /* 28px dot + 4px 间距 */
2370
+ bottom: 0;
2371
+ width: 1px;
2372
+ background: var(--token-outline-soft);
2373
+ }
2374
+ .timeline-dot {
2375
+ width: 28px; height: 28px; flex-shrink: 0;
2376
+ border-radius: var(--radius-full);
2377
+ display: flex; align-items: center; justify-content: center;
2378
+ background: var(--token-control);
2379
+ border: 1px solid var(--token-outline-soft);
2380
+ color: var(--token-text-muted);
2381
+ z-index: 1;
2382
+ }
2383
+ .timeline-dot svg { width: 13px; height: 13px; }
2384
+ .timeline-dot--accent { background: var(--token-accent); color: var(--token-on-accent); border-color: transparent; }
2385
+ .timeline-dot--success { background: var(--token-success); color: var(--token-on-accent); border-color: transparent; }
2386
+ .timeline-dot--warning { background: var(--token-warning); color: var(--token-on-accent); border-color: transparent; }
2387
+ .timeline-dot--error { background: var(--token-error); color: var(--token-on-accent); border-color: transparent; }
2388
+ .timeline-dot--info { background: var(--token-info); color: var(--token-on-accent); border-color: transparent; }
2389
+ .timeline-body { flex: 1; min-width: 0; }
2390
+ .timeline-head { display: flex; align-items: center; gap: 8px; }
2391
+ .timeline-title { font-size: 13px; font-weight: 500; color: var(--token-text); }
2392
+ .timeline-time { font-size: 11px; color: var(--token-text-faint); margin-left: auto; font-variant-numeric: tabular-nums; }
2393
+ .timeline-desc { font-size: 12px; color: var(--token-text-muted); margin: 4px 0 0; }
2394
+
2395
+ /* ══════════════ desc · 描述列表 ══════════════
2396
+ * 列数由容器 style="--desc-cols:N" 指定,默认 2;item 可用 style="--desc-span:N" 跨列。
2397
+ * --bordered 变体:gap:1px 露出容器底色形成格线。
2398
+ */
2399
+ .desc { display: flex; flex-direction: column; gap: 8px; }
2400
+ .desc-title { font-size: 13px; font-weight: 500; color: var(--token-text); margin: 0; }
2401
+ .desc-grid {
2402
+ display: grid;
2403
+ grid-template-columns: repeat(var(--desc-cols, 2), minmax(0, 1fr));
2404
+ gap: 1px;
2405
+ margin: 0;
2406
+ }
2407
+ .desc--bordered .desc-grid {
2408
+ background: var(--token-outline-soft);
2409
+ border: 1px solid var(--token-outline-soft);
2410
+ border-radius: var(--radius-lg);
2411
+ overflow: hidden;
2412
+ }
2413
+ .desc-item { grid-column: span var(--desc-span, 1); }
2414
+ .desc--bordered .desc-item { background: var(--token-card); padding: 8px 12px; }
2415
+ .desc:not(.desc--bordered) .desc-item { display: flex; flex-direction: column; gap: 2px; }
2416
+ .desc-item dt { font-size: 11px; color: var(--token-text-faint); }
2417
+ .desc-item dd { font-size: 13px; color: var(--token-text); margin: 0; }
2418
+
2419
+ /* ══════════════ tree · 树形(behaviors/tree 配套) ══════════════
2420
+ * DOM 契约:
2421
+ * <ul class="tree"><li>
2422
+ * <div class="tree-node" style="--depth:0">
2423
+ * <button class="tree-toggle"><svg/></button> <!-- 叶子用空 span.tree-toggle 占 18px -->
2424
+ * <span class="tree-label">节点</span>
2425
+ * </div>
2426
+ * <ul>…子节点…</ul> <!-- 可选;behavior 切换 hidden -->
2427
+ * </li></ul>
2428
+ * 折叠 = toggle 挂 .is-collapsed(chevron 转 -90°)+ 子 ul hidden;选中 = node 挂 .is-selected。
2429
+ */
2430
+ .tree, .tree ul { list-style: none; margin: 0; padding: 0; }
2431
+ .tree { user-select: none; }
2432
+ .tree-node {
2433
+ display: flex; align-items: center; gap: 6px;
2434
+ padding: 6px 8px 6px calc(4px + var(--depth, 0) * 14px);
2435
+ border-radius: var(--radius-md);
2436
+ font-size: 13px;
2437
+ color: var(--token-text);
2438
+ cursor: pointer;
2439
+ transition: background-color var(--duration-normal) var(--ease-out);
2440
+ }
2441
+ .tree-node:hover { background: var(--token-control-hover); }
2442
+ .tree-node.is-selected {
2443
+ background: var(--token-accent-subtle);
2444
+ color: var(--token-accent);
2445
+ font-weight: 500;
2446
+ }
2447
+ .tree-node.is-disabled { opacity: 0.4; pointer-events: none; }
2448
+ .tree-toggle {
2449
+ width: 18px; height: 18px; flex-shrink: 0;
2450
+ display: inline-flex; align-items: center; justify-content: center;
2451
+ padding: 0; border: 0; background: none;
2452
+ color: var(--token-text-muted);
2453
+ cursor: pointer;
2454
+ }
2455
+ .tree-toggle:hover { color: var(--token-text); }
2456
+ .tree-toggle svg {
2457
+ width: 13px; height: 13px;
2458
+ transition: transform var(--duration-normal) var(--ease-out);
2459
+ }
2460
+ .tree-toggle.is-collapsed svg { transform: rotate(-90deg); }
2461
+ .tree-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2462
+
2463
+
2464
+ /* ── components/empty.css ── */
2465
+ /*
2466
+ * @icen.ai/ui — Component: empty(空状态)
2467
+ * 包含:.empty 容器 + .empty-icon / .empty-title / .empty-desc / .empty-action
2468
+ * 规格源自 opengal src/presentation/ui/data/display.tsx 的 EmptyState。
2469
+ */
2470
+
2471
+ .empty {
2472
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
2473
+ text-align: center;
2474
+ padding: 48px 16px;
2475
+ }
2476
+ .empty-icon {
2477
+ width: 48px; height: 48px;
2478
+ border-radius: var(--radius-lg);
2479
+ background: var(--token-card);
2480
+ border: 1px solid var(--token-outline-soft);
2481
+ box-shadow: var(--token-shadow-inset);
2482
+ display: flex; align-items: center; justify-content: center;
2483
+ color: var(--token-text-faint);
2484
+ margin-bottom: 16px;
2485
+ }
2486
+ .empty-icon svg { width: 24px; height: 24px; }
2487
+ .empty-title { font-size: 14px; font-weight: 500; color: var(--token-text); margin: 0; }
2488
+ .empty-desc { font-size: 12px; color: var(--token-text-muted); margin: 4px 0 0; max-width: 240px; }
2489
+ .empty-action { margin-top: 16px; }
2490
+
2491
+
2492
+ /* ── components/feedback.css ── */
2493
+ /*
2494
+ * @icen.ai/ui — Component: feedback(反馈族)
2495
+ * 包含:alert(四态内联横幅)/ result(终态页)/ progress(线性进度)/ spinner(加载圈)/ skeleton(占位块)
2496
+ * 规格源自 opengal src/presentation/ui/feedback/toast.tsx 的 Alert 与 data/exhibit.tsx 的 Result、
2497
+ * data/display.tsx 的 Progress / Spinner / Skeleton。
2498
+ * base.css 未提供 spin / skeleton-pulse 关键帧,在本文件自带。
2499
+ */
2500
+
2501
+ /* ══════════════ alert · 内联横幅 ══════════════ */
2502
+ .alert {
2503
+ display: flex; align-items: flex-start; gap: 12px;
2504
+ padding: 12px;
2505
+ border-radius: var(--radius-lg);
2506
+ box-shadow: var(--token-shadow-inset);
2507
+ border: 1px solid transparent;
2508
+ }
2509
+ .alert--info { background: var(--token-info-muted); border-color: color-mix(in srgb, var(--token-info) 15%, transparent); }
2510
+ .alert--success { background: var(--token-success-muted); border-color: color-mix(in srgb, var(--token-success) 15%, transparent); }
2511
+ .alert--warning { background: var(--token-warning-muted); border-color: color-mix(in srgb, var(--token-warning) 15%, transparent); }
2512
+ .alert--error { background: var(--token-error-muted); border-color: color-mix(in srgb, var(--token-error) 15%, transparent); }
2513
+
2514
+ .alert-icon { flex-shrink: 0; margin-top: 2px; display: flex; }
2515
+ .alert-icon svg { width: 16px; height: 16px; }
2516
+ .alert--info .alert-icon, .alert--info .alert-title { color: var(--token-info); }
2517
+ .alert--success .alert-icon, .alert--success .alert-title { color: var(--token-success); }
2518
+ .alert--warning .alert-icon, .alert--warning .alert-title { color: var(--token-warning); }
2519
+ .alert--error .alert-icon, .alert--error .alert-title { color: var(--token-error); }
2520
+
2521
+ .alert-content { flex: 1; min-width: 0; }
2522
+ .alert-title { font-size: 13px; font-weight: 500; margin: 0; }
2523
+ .alert-body { font-size: 12px; color: var(--token-text-muted); line-height: 1.6; }
2524
+ .alert-close {
2525
+ margin-left: auto; flex-shrink: 0;
2526
+ display: flex; align-items: center; justify-content: center;
2527
+ padding: 2px; border: 0; border-radius: var(--radius-xs);
2528
+ background: none; color: var(--token-text-faint); cursor: pointer;
2529
+ }
2530
+ .alert-close:hover { color: var(--token-text); background: color-mix(in srgb, var(--token-text) 6%, transparent); }
2531
+ .alert-close svg { width: 14px; height: 14px; }
2532
+
2533
+ /* ══════════════ result · 终态页 ══════════════ */
2534
+ .result {
2535
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
2536
+ text-align: center;
2537
+ padding: 40px 24px;
2538
+ }
2539
+ .result-icon {
2540
+ width: 64px; height: 64px;
2541
+ border-radius: var(--radius-full);
2542
+ display: flex; align-items: center; justify-content: center;
2543
+ box-shadow: var(--token-shadow-inset);
2544
+ border: 1px solid transparent;
2545
+ margin-bottom: 16px;
2546
+ }
2547
+ .result-icon svg { width: 28px; height: 28px; }
2548
+ .result--success .result-icon { background: var(--token-success-muted); color: var(--token-success); border-color: color-mix(in srgb, var(--token-success) 10%, transparent); }
2549
+ .result--error .result-icon { background: var(--token-error-muted); color: var(--token-error); border-color: color-mix(in srgb, var(--token-error) 10%, transparent); }
2550
+ .result--warning .result-icon { background: var(--token-warning-muted); color: var(--token-warning); border-color: color-mix(in srgb, var(--token-warning) 10%, transparent); }
2551
+ .result--info .result-icon { background: var(--token-info-muted); color: var(--token-info); border-color: color-mix(in srgb, var(--token-info) 10%, transparent); }
2552
+ .result-title { font-size: 17px; font-weight: 600; color: var(--token-text); margin: 0; }
2553
+ .result-desc { font-size: 13px; color: var(--token-text-muted); margin: 4px 0 0; max-width: 360px; }
2554
+ .result-extra { display: flex; align-items: center; gap: 8px; margin-top: 20px; }
2555
+
2556
+ /* ══════════════ progress · 线性进度 ══════════════ */
2557
+ .progress { display: flex; align-items: center; gap: 8px; }
2558
+ .progress-track {
2559
+ flex: 1; height: 6px; overflow: hidden;
2560
+ border-radius: var(--radius-full);
2561
+ background: color-mix(in srgb, var(--token-line-soft) 40%, transparent);
2562
+ }
2563
+ .progress-bar {
2564
+ height: 100%;
2565
+ border-radius: var(--radius-full);
2566
+ background: var(--token-accent);
2567
+ transition: width 500ms var(--ease-out);
2568
+ }
2569
+ .progress--sm .progress-track { height: 4px; }
2570
+ .progress-label { font-size: 11px; color: var(--token-text-muted); font-variant-numeric: tabular-nums; }
2571
+
2572
+ /* ══════════════ spinner · 加载圈 ══════════════ */
2573
+ .spinner {
2574
+ width: 20px; height: 20px;
2575
+ border: 2px solid var(--token-accent);
2576
+ border-top-color: transparent;
2577
+ border-radius: var(--radius-full);
2578
+ animation: icen-spin 1s linear infinite;
2579
+ }
2580
+ .spinner--sm { width: 16px; height: 16px; }
2581
+ .spinner--lg { width: 32px; height: 32px; }
2582
+
2583
+ @keyframes icen-spin {
2584
+ to { transform: rotate(360deg); }
2585
+ }
2586
+
2587
+ /* ══════════════ skeleton · 占位块 ══════════════ */
2588
+ .skeleton {
2589
+ background: var(--token-control-hover);
2590
+ border-radius: var(--radius-sm);
2591
+ animation: icen-skeleton-pulse 2s ease-in-out infinite;
2592
+ }
2593
+ .skeleton--circle { border-radius: var(--radius-full); }
2594
+
2595
+ @keyframes icen-skeleton-pulse {
2596
+ 0%, 100% { opacity: 1; }
2597
+ 50% { opacity: 0.5; }
2598
+ }
2599
+
2600
+
2003
2601
  /* ── components/form.css ── */
2004
2602
  /*
2005
2603
  * @icen.ai/ui — Component: form(表单字段/输入/错误与成功提示)
@@ -2034,6 +2632,634 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
2034
2632
  .banner-ok, .banner-err { margin-bottom: 20px; }
2035
2633
 
2036
2634
 
2635
+ /* ── components/input.css ── */
2636
+ /*
2637
+ * @icen.ai/ui — Component: input(wrapper 式输入框 / 多行 / 搜索)
2638
+ * 规格源自 opengal primitives/input.tsx;与 behaviors/input.ts 配套。
2639
+ *
2640
+ * 契约:
2641
+ * <div class="input-wrap control [--sm|--lg] [.is-error] [.is-disabled]">
2642
+ * <span class="input-leading">…</span> 可选前缀
2643
+ * <input class="input" />
2644
+ * <button class="input-clear">…</button> 可选清除钮(behavior 清空并聚焦)
2645
+ * <button class="input-pw-toggle">…</button> 可选密码可见切换(behavior 驱动)
2646
+ * <span class="input-trailing">…</span> 可选后缀
2647
+ * </div>
2648
+ * 多行:<div class="textarea-wrap control"><textarea class="textarea" data-autosize></textarea></div>
2649
+ * 搜索:.input-wrap--search = leading 放大镜 + .input-clear 的结构约定(无额外视觉)
2650
+ */
2651
+
2652
+ .input-wrap {
2653
+ display: flex; align-items: center; gap: 8px; width: 100%;
2654
+ height: 36px; padding: 0 12px; font-size: 13px;
2655
+ background: var(--token-control);
2656
+ border: 1px solid var(--token-outline-soft);
2657
+ border-radius: var(--radius-lg);
2658
+ box-shadow: var(--token-shadow-inset);
2659
+ }
2660
+ .input-wrap:focus-within { border-color: var(--token-accent); }
2661
+ .input-wrap.is-error, .input-wrap.is-error:focus-within { border-color: var(--token-error); }
2662
+ .input-wrap.is-disabled { opacity: 0.4; }
2663
+ .input-wrap.is-disabled .input { cursor: not-allowed; }
2664
+
2665
+ /* 尺寸三档:高度 / 横向内边距 / 字号 */
2666
+ .input-wrap--sm { height: 32px; padding: 0 10px; font-size: 12px; }
2667
+ .input-wrap--lg { height: 40px; padding: 0 14px; font-size: 14px; }
2668
+
2669
+ .input {
2670
+ flex: 1; min-width: 0; padding: 0;
2671
+ background: transparent; border: none; outline: none;
2672
+ color: var(--token-text); font: inherit;
2673
+ }
2674
+ .input::placeholder { color: color-mix(in srgb, var(--token-text-faint) 60%, transparent); }
2675
+
2676
+ .input-leading, .input-trailing {
2677
+ display: inline-flex; align-items: center; flex-shrink: 0;
2678
+ color: var(--token-text-faint);
2679
+ }
2680
+
2681
+ .input-clear {
2682
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
2683
+ padding: 2px; border: none; background: transparent; cursor: pointer;
2684
+ border-radius: var(--radius-full); color: var(--token-text-faint);
2685
+ transition: color var(--duration-normal) var(--ease-out),
2686
+ background-color var(--duration-normal) var(--ease-out);
2687
+ }
2688
+ .input-clear:hover { color: var(--token-text); background: var(--token-surface-hover); }
2689
+
2690
+ .input-pw-toggle {
2691
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
2692
+ padding: 2px; border: none; background: transparent; cursor: pointer;
2693
+ color: var(--token-text-faint);
2694
+ transition: color var(--duration-normal) var(--ease-out);
2695
+ }
2696
+ .input-pw-toggle:hover { color: var(--token-text); }
2697
+
2698
+ /* ── 多行:同一 wrapper 视觉,内部 textarea 裸奔 ── */
2699
+ .textarea-wrap {
2700
+ display: flex; width: 100%;
2701
+ background: var(--token-control);
2702
+ border: 1px solid var(--token-outline-soft);
2703
+ border-radius: var(--radius-lg);
2704
+ box-shadow: var(--token-shadow-inset);
2705
+ }
2706
+ .textarea-wrap:focus-within { border-color: var(--token-accent); }
2707
+ .textarea-wrap.is-error, .textarea-wrap.is-error:focus-within { border-color: var(--token-error); }
2708
+ .textarea-wrap.is-disabled { opacity: 0.4; }
2709
+ .textarea-wrap.is-disabled .textarea { cursor: not-allowed; }
2710
+
2711
+ .textarea {
2712
+ width: 100%; padding: 12px;
2713
+ background: transparent; border: none; outline: none; resize: none;
2714
+ color: var(--token-text); font: inherit; font-size: 13px; line-height: 1.5;
2715
+ min-height: calc(3 * 1.5em + 24px); /* 3 行 + 上下 padding */
2716
+ }
2717
+ .textarea::placeholder { color: color-mix(in srgb, var(--token-text-faint) 60%, transparent); }
2718
+
2719
+
2720
+ /* ── components/layout.css ── */
2721
+ /*
2722
+ * @icen.ai/ui — Component: layout(布局原语,全部是工具类,无 JS)
2723
+ * stack / divider / surface / text / grid / center / container / aspect / visually-hidden
2724
+ * 全部消费 --token-*;档位类名与 opengal primitives/layout(-containers) 一一对应。
2725
+ */
2726
+
2727
+ /* ── Stack:flex 堆叠(默认列向,gap 8px) ── */
2728
+ .stack { display: flex; flex-direction: column; gap: 8px; }
2729
+ .stack--row { flex-direction: row; }
2730
+ .stack--inline { display: inline-flex; }
2731
+ .stack--wrap { flex-wrap: wrap; }
2732
+ .stack--gap-0 { gap: 0; }
2733
+ .stack--gap-1 { gap: 4px; }
2734
+ .stack--gap-2 { gap: 8px; }
2735
+ .stack--gap-3 { gap: 12px; }
2736
+ .stack--gap-4 { gap: 16px; }
2737
+ .stack--gap-6 { gap: 24px; }
2738
+ .stack--gap-8 { gap: 32px; }
2739
+ .stack--gap-12 { gap: 48px; }
2740
+ .stack--gap-16 { gap: 64px; }
2741
+ .stack--items-start { align-items: flex-start; }
2742
+ .stack--items-center { align-items: center; }
2743
+ .stack--items-end { align-items: flex-end; }
2744
+ .stack--items-stretch { align-items: stretch; }
2745
+ .stack--items-baseline { align-items: baseline; }
2746
+ .stack--justify-start { justify-content: flex-start; }
2747
+ .stack--justify-center { justify-content: center; }
2748
+ .stack--justify-end { justify-content: flex-end; }
2749
+ .stack--justify-between { justify-content: space-between; }
2750
+ .stack--justify-around { justify-content: space-around; }
2751
+ .stack--justify-evenly { justify-content: space-evenly; }
2752
+
2753
+ /* ── Divider:分隔线 ── */
2754
+ .divider { background: var(--token-outline-soft); height: 1px; width: 100%; }
2755
+ .divider--vertical { width: 1px; height: 100%; align-self: stretch; }
2756
+
2757
+ /* ── Surface:卡片容器(默认 inset 阴影 + 16px padding) ── */
2758
+ .surface {
2759
+ border-radius: var(--radius-lg);
2760
+ background: var(--token-card);
2761
+ border: 1px solid var(--token-outline-soft);
2762
+ box-shadow: var(--token-shadow-inset);
2763
+ padding: 16px;
2764
+ }
2765
+ .surface--elevated { box-shadow: var(--token-shadow-card); }
2766
+ .surface--overlay {
2767
+ background: var(--token-card-strong);
2768
+ border-color: var(--token-outline);
2769
+ box-shadow: var(--token-shadow-overlay);
2770
+ }
2771
+ .surface--p-none { padding: 0; }
2772
+ .surface--p-sm { padding: 12px; }
2773
+ .surface--p-md { padding: 16px; }
2774
+ .surface--p-lg { padding: 20px; }
2775
+
2776
+ /* ── Text:字号 / 语义色 / 截断 ── */
2777
+ .text--xs { font-size: 11px; }
2778
+ .text--sm { font-size: 12px; }
2779
+ .text--base { font-size: 13px; }
2780
+ .text--md { font-size: 14px; }
2781
+ .text--lg { font-size: 15px; }
2782
+ .text--xl { font-size: 18px; }
2783
+ .text--2xl { font-size: 22px; }
2784
+ .text--muted { color: var(--token-text-muted); }
2785
+ .text--faint { color: var(--token-text-faint); }
2786
+ .text--accent { color: var(--token-accent); }
2787
+ .text--success { color: var(--token-success); }
2788
+ .text--warning { color: var(--token-warning); }
2789
+ .text--error { color: var(--token-error); }
2790
+ .text--truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2791
+
2792
+ /* ── Grid:栅格(默认 gap 12px) ── */
2793
+ .grid { display: grid; gap: 12px; }
2794
+ .grid--cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
2795
+ .grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2796
+ .grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2797
+ .grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2798
+ .grid--cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
2799
+ .grid--cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
2800
+ .grid--gap-0 { gap: 0; }
2801
+ .grid--gap-4 { gap: 4px; }
2802
+ .grid--gap-8 { gap: 8px; }
2803
+ .grid--gap-12 { gap: 12px; }
2804
+ .grid--gap-16 { gap: 16px; }
2805
+ .grid--gap-24 { gap: 24px; }
2806
+
2807
+ /* ── Center:flex 居中盒 ── */
2808
+ .center { display: flex; align-items: center; justify-content: center; }
2809
+
2810
+ /* ── Container:限宽容器(默认 960px) ── */
2811
+ .container { width: 100%; margin-inline: auto; padding-inline: 16px; max-width: 960px; }
2812
+ .container--sm { max-width: 480px; }
2813
+ .container--md { max-width: 720px; }
2814
+ .container--lg { max-width: 960px; }
2815
+ .container--xl { max-width: 1200px; }
2816
+ .container--full { max-width: none; }
2817
+
2818
+ /* ── Aspect:固定比例盒(padding-bottom hack,内容 absolute inset:0) ── */
2819
+ .aspect { position: relative; width: 100%; overflow: hidden; padding-bottom: 56.25%; }
2820
+ .aspect--square { padding-bottom: 100%; }
2821
+ .aspect--4-3 { padding-bottom: 75%; }
2822
+ .aspect--2-3 { padding-bottom: 150%; }
2823
+ .aspect > * { position: absolute; inset: 0; }
2824
+
2825
+ /* ── VisuallyHidden:屏幕阅读器专用文本(sr-only 等价) ── */
2826
+ .visually-hidden {
2827
+ position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
2828
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
2829
+ }
2830
+
2831
+
2832
+ /* ── components/media.css ── */
2833
+ /*
2834
+ * @icen.ai/ui — Component: media(媒体族)
2835
+ * 包含:avatar(头像)/ media-card(封面卡,滑出 overlay)/ rating(星级评分)/ carousel(轮播,与 behaviors/carousel 配套)
2836
+ * carousel 归入本文件(无独立 carousel.css 配额);规格源自 opengal data/display.tsx 的 Avatar、
2837
+ * data/cartridge-card.tsx 的 hover overlay、data/exhibit.tsx 的 Carousel 与 Rating。
2838
+ */
2839
+
2840
+ /* ══════════════ avatar · 头像 ══════════════ */
2841
+ .avatar {
2842
+ position: relative;
2843
+ display: inline-flex; align-items: center; justify-content: center;
2844
+ overflow: hidden;
2845
+ border-radius: var(--radius-full);
2846
+ background: var(--token-accent-subtle);
2847
+ color: var(--token-accent);
2848
+ font-weight: 600;
2849
+ width: 36px; height: 36px; font-size: 12px;
2850
+ flex-shrink: 0;
2851
+ }
2852
+ .avatar--xs { width: 20px; height: 20px; font-size: 9px; }
2853
+ .avatar--sm { width: 28px; height: 28px; font-size: 10px; }
2854
+ .avatar--lg { width: 48px; height: 48px; font-size: 14px; }
2855
+ .avatar img { width: 100%; height: 100%; object-fit: cover; }
2856
+
2857
+ /* ══════════════ media-card · 封面卡 ══════════════ */
2858
+ .media-card {
2859
+ position: relative;
2860
+ overflow: hidden;
2861
+ border-radius: var(--radius-lg);
2862
+ transition: transform 300ms var(--ease-out), box-shadow 300ms var(--ease-out);
2863
+ }
2864
+ .media-card:hover {
2865
+ transform: translateY(-6px);
2866
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2), 0 0 20px var(--token-accent-glow);
2867
+ }
2868
+ .media-card-cover {
2869
+ display: block;
2870
+ width: 100%;
2871
+ aspect-ratio: 2 / 3;
2872
+ object-fit: cover;
2873
+ }
2874
+ .media-card-overlay {
2875
+ position: absolute; left: 0; right: 0; bottom: 0;
2876
+ padding: 32px 12px 10px;
2877
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.35) 65%, transparent);
2878
+ backdrop-filter: blur(10px);
2879
+ -webkit-backdrop-filter: blur(10px);
2880
+ color: #fff;
2881
+ transform: translateY(100%);
2882
+ transition: transform 200ms var(--ease-out);
2883
+ pointer-events: none;
2884
+ }
2885
+ .media-card:hover .media-card-overlay {
2886
+ transform: translateY(0);
2887
+ transition-delay: 80ms;
2888
+ }
2889
+
2890
+ /* ══════════════ rating · 星级评分 ══════════════ */
2891
+ .rating { display: inline-flex; align-items: center; gap: 2px; }
2892
+ .rating-star {
2893
+ padding: 2px;
2894
+ border-radius: var(--radius-xs);
2895
+ background: none; border: 0;
2896
+ cursor: pointer;
2897
+ display: inline-flex; align-items: center; justify-content: center;
2898
+ }
2899
+ .rating-star svg {
2900
+ width: 18px; height: 18px; display: block;
2901
+ fill: transparent;
2902
+ stroke: var(--token-line-strong);
2903
+ stroke-width: 1.6;
2904
+ transition: fill var(--duration-normal) var(--ease-out), stroke var(--duration-normal) var(--ease-out);
2905
+ }
2906
+ .rating--sm .rating-star svg { width: 14px; height: 14px; }
2907
+ .rating--lg .rating-star svg { width: 22px; height: 22px; }
2908
+ .rating-star.is-filled svg { fill: var(--token-warning); stroke: var(--token-warning); }
2909
+ .rating-star.is-half svg {
2910
+ fill: color-mix(in srgb, var(--token-warning) 50%, transparent);
2911
+ stroke: var(--token-warning);
2912
+ }
2913
+
2914
+ /* ══════════════ carousel · 轮播(behaviors/carousel 配套) ══════════════
2915
+ * DOM 契约:
2916
+ * <div class="carousel">
2917
+ * <div class="carousel-track"><div class="carousel-slide">…</div>×n</div>
2918
+ * <button class="carousel-arrow carousel-arrow--prev">…</button>
2919
+ * <button class="carousel-arrow carousel-arrow--next">…</button>
2920
+ * <div class="carousel-dots"></div> <!-- dots 由 behavior 自动生成 -->
2921
+ * </div>
2922
+ */
2923
+ .carousel {
2924
+ position: relative;
2925
+ overflow: hidden;
2926
+ border-radius: var(--radius-lg);
2927
+ background: var(--token-control);
2928
+ }
2929
+ .carousel-track {
2930
+ display: flex;
2931
+ transition: transform 300ms var(--ease-out);
2932
+ }
2933
+ .carousel-slide { width: 100%; flex-shrink: 0; }
2934
+ .carousel-arrow {
2935
+ position: absolute; top: 50%; transform: translateY(-50%);
2936
+ width: 32px; height: 32px;
2937
+ border-radius: var(--radius-full);
2938
+ background: color-mix(in srgb, var(--token-card) 78%, transparent);
2939
+ border: 1px solid var(--token-outline-soft);
2940
+ color: var(--token-text);
2941
+ display: flex; align-items: center; justify-content: center;
2942
+ backdrop-filter: blur(4px);
2943
+ -webkit-backdrop-filter: blur(4px);
2944
+ box-shadow: var(--token-shadow-card);
2945
+ cursor: pointer;
2946
+ transition: background-color var(--duration-normal) var(--ease-out), transform var(--duration-normal) var(--ease-out);
2947
+ }
2948
+ .carousel-arrow:hover { background: var(--token-card-strong); }
2949
+ .carousel-arrow:active { transform: translateY(-50%) scale(0.95); }
2950
+ .carousel-arrow svg { width: 16px; height: 16px; }
2951
+ .carousel-arrow--prev { left: 8px; }
2952
+ .carousel-arrow--next { right: 8px; }
2953
+ .carousel-dots {
2954
+ position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
2955
+ display: flex; gap: 6px;
2956
+ }
2957
+ .carousel-dot {
2958
+ width: 6px; height: 6px; padding: 0;
2959
+ border: 0; border-radius: var(--radius-full);
2960
+ background: color-mix(in srgb, var(--token-card) 70%, transparent);
2961
+ cursor: pointer;
2962
+ transition: width var(--duration-normal) var(--ease-out), background-color var(--duration-normal) var(--ease-out);
2963
+ }
2964
+ .carousel-dot:hover { background: color-mix(in srgb, var(--token-accent) 70%, transparent); }
2965
+ .carousel-dot.active { width: 20px; background: var(--token-accent); }
2966
+
2967
+
2968
+ /* ── components/menu.css ── */
2969
+ /*
2970
+ * @icen.ai/ui — Component: menu(菜单面板,与 behaviors/dropdown、context-menu 配套)
2971
+ * 面板由 JS portal 到 document.body 并按 fixed 定位;data-side 决定进场位移方向。
2972
+ * 规格源自 opengal overlay/dropdown + overlay/context-menu。
2973
+ */
2974
+
2975
+ .menu {
2976
+ position: fixed; z-index: var(--z-popover);
2977
+ min-width: 180px; max-height: min(70vh, 420px); overflow-y: auto;
2978
+ margin: 0; padding: 4px;
2979
+ background: color-mix(in srgb, var(--token-card-strong) 92%, transparent);
2980
+ backdrop-filter: blur(24px);
2981
+ border: 1px solid var(--token-outline);
2982
+ box-shadow: var(--token-shadow-overlay);
2983
+ border-radius: var(--radius-lg);
2984
+ animation: icen-menu-in 150ms var(--ease-out);
2985
+ }
2986
+ .menu[data-side='top'] { animation-name: icen-menu-in-top; }
2987
+
2988
+ /* 右键菜单修正(opengal context-menu:min 218 / max 286 / z 9999 / 入场 100ms) */
2989
+ .menu--context {
2990
+ min-width: 218px; max-width: 286px; z-index: 9999;
2991
+ animation-duration: 100ms;
2992
+ }
2993
+
2994
+ /* ── 分组标签 / 头(context-menu 的 header 项) ── */
2995
+ .menu-label {
2996
+ padding: 4px 8px; font-size: 10px; font-weight: 600;
2997
+ text-transform: uppercase; letter-spacing: 0.08em;
2998
+ color: var(--token-text-faint);
2999
+ }
3000
+ .menu-header { padding: 8px 10px 6px; }
3001
+ .menu-header-label {
3002
+ display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3003
+ font-size: 11px; font-weight: 500; line-height: 16px; color: var(--token-text);
3004
+ }
3005
+ .menu-header-desc {
3006
+ display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3007
+ font-size: 10px; line-height: 14px; color: var(--token-text-faint);
3008
+ }
3009
+
3010
+ /* ── 菜单项 ── */
3011
+ .menu-item {
3012
+ position: relative; display: flex; align-items: center; gap: 8px;
3013
+ width: 100%; padding: 6px 8px;
3014
+ background: none; border: 0; font: inherit; font-size: 12px; text-align: left;
3015
+ color: var(--token-text);
3016
+ border-radius: var(--radius-md); cursor: pointer; user-select: none;
3017
+ }
3018
+ .menu-item.is-highlighted, .menu-item:focus {
3019
+ background: var(--token-surface-hover); color: var(--token-accent); outline: none;
3020
+ }
3021
+ .menu-item--danger { color: var(--token-error); }
3022
+ .menu-item--danger.is-highlighted, .menu-item--danger:focus {
3023
+ background: var(--token-error-muted); color: var(--token-error);
3024
+ }
3025
+ .menu-item.is-disabled, .menu-item:disabled { opacity: 0.5; pointer-events: none; }
3026
+
3027
+ .menu-item-icon {
3028
+ display: inline-flex; align-items: center; justify-content: center;
3029
+ width: 14px; height: 14px; flex-shrink: 0;
3030
+ }
3031
+ .menu-item-icon:empty { width: 16px; height: auto; } /* 无图标时 16px 宽占位 */
3032
+ .menu-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
3033
+ .menu-item-sub { display: block; font-size: 10px; line-height: 1.2; color: var(--token-text-faint); }
3034
+ .menu-item-shortcut { font-size: 10px; color: var(--token-text-faint); }
3035
+
3036
+ /* 勾选项:左勾 ✓ + 缩进 */
3037
+ .menu-item-check {
3038
+ position: absolute; left: 8px; top: 50%; transform: translateY(-50%);
3039
+ color: var(--token-accent); font-size: 12px; line-height: 1;
3040
+ }
3041
+ .menu-item--checked { padding-left: 28px; }
3042
+
3043
+ .menu-separator { margin: 6px 8px; height: 1px; background: var(--token-line-soft); }
3044
+
3045
+ /* ── 进场:fade + scale(.95→1),bottom 从上方 8px 滑入、top 从下方 8px 滑入 ── */
3046
+ @keyframes icen-menu-in {
3047
+ from { opacity: 0; transform: scale(0.95) translateY(-8px); }
3048
+ to { opacity: 1; transform: scale(1) translateY(0); }
3049
+ }
3050
+ @keyframes icen-menu-in-top {
3051
+ from { opacity: 0; transform: scale(0.95) translateY(8px); }
3052
+ to { opacity: 1; transform: scale(1) translateY(0); }
3053
+ }
3054
+
3055
+
3056
+ /* ── components/modal.css ── */
3057
+ /*
3058
+ * @icen.ai/ui — Component: modal(对话框 + 贴边 Sheet,与 behaviors/modal 配套)
3059
+ * 契约:<div class="modal-backdrop" data-modal="id" hidden> > .modal[role=dialog]
3060
+ * 规格源自 opengal overlay/dialog:backdrop fadeIn 150ms、面板 dialogScaleIn
3061
+ * 200ms cubic-bezier(.16,1,.3,1)、Sheet 四向 slideIn 200ms 位移 40px。
3062
+ */
3063
+
3064
+ .modal-backdrop {
3065
+ position: fixed; inset: 0; z-index: var(--z-dialog);
3066
+ display: flex; align-items: center; justify-content: center; padding: 16px;
3067
+ background: rgba(0, 0, 0, 0.45); backdrop-filter: blur(4px);
3068
+ animation: icen-modal-fade-in 150ms ease-out;
3069
+ }
3070
+ .modal-backdrop[hidden] { display: none; }
3071
+
3072
+ .modal {
3073
+ display: flex; flex-direction: column; overflow: hidden;
3074
+ width: 100%; max-width: 480px; max-height: calc(100vh - 32px);
3075
+ background: var(--token-card-strong); color: var(--token-text);
3076
+ border: 1px solid var(--token-outline);
3077
+ box-shadow: var(--token-shadow-overlay);
3078
+ border-radius: var(--radius-xl);
3079
+ animation: icen-modal-scale-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
3080
+ }
3081
+ .modal:focus { outline: none; }
3082
+
3083
+ /* ── 尺寸档 ── */
3084
+ .modal--sm { max-width: 360px; }
3085
+ .modal--lg { max-width: 640px; }
3086
+ .modal--xl { max-width: 800px; }
3087
+ .modal--full { width: calc(100vw - 32px); max-width: none; height: calc(100vh - 32px); max-height: none; }
3088
+
3089
+ /* ── 头部 / 主体 / 底栏 ── */
3090
+ .modal-header {
3091
+ display: flex; align-items: flex-start; justify-content: space-between;
3092
+ gap: 16px; padding: 20px 24px 12px;
3093
+ }
3094
+ .modal-title { margin: 0; font-size: 15px; font-weight: 600; }
3095
+ .modal-description { margin: 2px 0 0; font-size: 12px; color: var(--token-text-muted); }
3096
+ .modal-close {
3097
+ display: inline-flex; align-items: center; justify-content: center;
3098
+ width: 28px; height: 28px; padding: 0; flex-shrink: 0;
3099
+ background: none; border: 0; cursor: pointer;
3100
+ color: var(--token-text-faint); font-size: 16px; line-height: 1;
3101
+ border-radius: var(--radius-md);
3102
+ }
3103
+ .modal-close:hover { background: var(--token-surface-hover); color: var(--token-text); }
3104
+
3105
+ .modal-body { flex: 1; overflow-y: auto; padding: 0 24px 20px; }
3106
+ .modal-footer {
3107
+ display: flex; justify-content: flex-end; gap: 8px; padding: 16px 24px;
3108
+ border-top: 1px solid color-mix(in srgb, var(--token-line-soft) 40%, transparent);
3109
+ }
3110
+
3111
+ /* ── Sheet 变体:面板 fixed 贴边,radius 只保留非贴边侧 ── */
3112
+ .modal--sheet-left, .modal--sheet-right, .modal--sheet-top, .modal--sheet-bottom {
3113
+ position: fixed; max-width: none; max-height: none; border-radius: 0;
3114
+ }
3115
+ .modal--sheet-left {
3116
+ left: 0; top: 0; bottom: 0; width: 360px; height: 100%;
3117
+ border-radius: 0 var(--radius-xl) var(--radius-xl) 0;
3118
+ animation-name: icen-modal-slide-left;
3119
+ }
3120
+ .modal--sheet-right {
3121
+ right: 0; top: 0; bottom: 0; width: 360px; height: 100%;
3122
+ border-radius: var(--radius-xl) 0 0 var(--radius-xl);
3123
+ animation-name: icen-modal-slide-right;
3124
+ }
3125
+ .modal--sheet-top {
3126
+ top: 0; left: 0; right: 0; width: 100%; height: 300px;
3127
+ border-radius: 0 0 var(--radius-xl) var(--radius-xl);
3128
+ animation-name: icen-modal-slide-top;
3129
+ }
3130
+ .modal--sheet-bottom {
3131
+ bottom: 0; left: 0; right: 0; width: 100%; height: 300px;
3132
+ border-radius: var(--radius-xl) var(--radius-xl) 0 0;
3133
+ animation-name: icen-modal-slide-bottom;
3134
+ }
3135
+ .modal--sheet-left.modal--sheet-sm, .modal--sheet-right.modal--sheet-sm { width: 280px; }
3136
+ .modal--sheet-left.modal--sheet-lg, .modal--sheet-right.modal--sheet-lg { width: 480px; }
3137
+ .modal--sheet-top.modal--sheet-lg, .modal--sheet-bottom.modal--sheet-lg { height: 50vh; }
3138
+
3139
+ /* ── 进场动画(与 opengal src/index.css 同参数) ── */
3140
+ @keyframes icen-modal-fade-in {
3141
+ from { opacity: 0; }
3142
+ to { opacity: 1; }
3143
+ }
3144
+ @keyframes icen-modal-scale-in {
3145
+ from { opacity: 0; transform: scale(0.96) translateY(8px); }
3146
+ to { opacity: 1; transform: scale(1) translateY(0); }
3147
+ }
3148
+ @keyframes icen-modal-slide-left {
3149
+ from { opacity: 0; transform: translateX(-40px); }
3150
+ to { opacity: 1; transform: translateX(0); }
3151
+ }
3152
+ @keyframes icen-modal-slide-right {
3153
+ from { opacity: 0; transform: translateX(40px); }
3154
+ to { opacity: 1; transform: translateX(0); }
3155
+ }
3156
+ @keyframes icen-modal-slide-top {
3157
+ from { opacity: 0; transform: translateY(-40px); }
3158
+ to { opacity: 1; transform: translateY(0); }
3159
+ }
3160
+ @keyframes icen-modal-slide-bottom {
3161
+ from { opacity: 0; transform: translateY(40px); }
3162
+ to { opacity: 1; transform: translateY(0); }
3163
+ }
3164
+
3165
+
3166
+ /* ── components/nav.css ── */
3167
+ /*
3168
+ * @icen.ai/ui — Component: nav(玻璃顶栏,scroll 收缩配套 behaviors/nav)
3169
+ * 用法:<nav class="nav nav--sticky" data-scroll-reactive>
3170
+ * <div class="nav-left">…</div><div class="nav-center">…</div><div class="nav-right">…</div>
3171
+ * </nav>
3172
+ * 三段:.nav-left / .nav-center(flex:1 居中)/ .nav-right,整行补充用 .nav-bottom。
3173
+ * 按钮:.nav-btn(HTML 组合 .motion .pressable 基元),.is-active / .is-disabled / .nav-btn--icon。
3174
+ * 契约:sticky/fixed 挂 --z-chrome 层;data-scroll-reactive 时 behaviors/nav 依 scrollY>12 切
3175
+ * .is-scrolled——未滚动更透且无边线,滚动后底色更实 + blur 16px。
3176
+ */
3177
+
3178
+ .nav {
3179
+ display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
3180
+ height: 56px; padding: 0 16px;
3181
+ background: var(--token-surface-glass);
3182
+ backdrop-filter: blur(12px) saturate(1.1);
3183
+ -webkit-backdrop-filter: blur(12px) saturate(1.1);
3184
+ border-bottom: 1px solid var(--token-outline-soft);
3185
+ transition: all var(--duration-slow) var(--ease-out);
3186
+ }
3187
+ /* 带整行 bottom 时放开固定高度,第一行仍保持 56px */
3188
+ .nav:has(.nav-bottom) { height: auto; min-height: 56px; }
3189
+ .nav--sticky { position: sticky; top: 0; z-index: var(--z-chrome); }
3190
+ .nav--fixed { position: fixed; top: 0; left: 0; right: 0; z-index: var(--z-chrome); }
3191
+
3192
+ /* scroll-reactive:未滚动更透、底边线透明(滚动后由 .is-scrolled 接管) */
3193
+ .nav[data-scroll-reactive]:not(.is-scrolled) {
3194
+ background: color-mix(in srgb, var(--token-surface-glass) 70%, transparent);
3195
+ backdrop-filter: blur(8px);
3196
+ -webkit-backdrop-filter: blur(8px);
3197
+ border-bottom-color: transparent;
3198
+ }
3199
+ .nav.is-scrolled {
3200
+ background: color-mix(in srgb, var(--token-surface-glass) 92%, transparent);
3201
+ backdrop-filter: blur(16px) saturate(1.2);
3202
+ -webkit-backdrop-filter: blur(16px) saturate(1.2);
3203
+ }
3204
+
3205
+ .nav-left, .nav-right { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
3206
+ .nav-center { flex: 1; display: flex; align-items: center; justify-content: center; gap: 4px; }
3207
+ .nav-bottom { width: 100%; }
3208
+
3209
+ .nav-btn {
3210
+ display: inline-flex; align-items: center; gap: 6px;
3211
+ border-radius: var(--radius-full); padding: 6px 12px;
3212
+ font-size: 13px; color: var(--token-text-muted);
3213
+ background: none; border: 0; cursor: pointer;
3214
+ }
3215
+ .nav-btn > svg { width: 14px; height: 14px; flex-shrink: 0; }
3216
+ .nav-btn:not(.is-active):hover { color: var(--token-text); background: var(--token-control-hover); }
3217
+ .nav-btn.is-active {
3218
+ background: var(--token-accent); color: var(--token-on-accent);
3219
+ font-weight: 500; box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
3220
+ }
3221
+ .nav-btn.is-disabled, .nav-btn:disabled { opacity: .4; pointer-events: none; }
3222
+ .nav-btn--icon { padding: 6px 10px; }
3223
+ .nav-btn--icon > svg { width: 15px; height: 15px; }
3224
+
3225
+ .nav-divider { width: 1px; height: 20px; margin: 0 2px; background: var(--token-outline-soft); }
3226
+
3227
+
3228
+ /* ── components/pagination.css ── */
3229
+ /*
3230
+ * @icen.ai/ui — Component: pagination(分页)
3231
+ * 用法:<nav class="pagination" aria-label="分页">
3232
+ * <button class="page-btn motion is-disabled">‹</button>
3233
+ * <button class="page-btn motion">1</button>
3234
+ * <span class="page-ellipsis">…</span>
3235
+ * <button class="page-btn motion is-active" aria-current="page">5</button>
3236
+ * <button class="page-btn motion">›</button>
3237
+ * </nav>
3238
+ * 翻页逻辑由消费方实现,这里只提供静态样式契约。
3239
+ */
3240
+
3241
+ .pagination { display: flex; align-items: center; gap: 4px; }
3242
+
3243
+ .page-btn {
3244
+ min-width: 28px; height: 28px; padding: 0 6px;
3245
+ border-radius: var(--radius-md); font-size: 12px;
3246
+ display: inline-flex; align-items: center; justify-content: center;
3247
+ color: var(--token-text-muted); background: none; border: 0; cursor: pointer;
3248
+ }
3249
+ .page-btn:not(.is-active):hover { background: var(--token-control-hover); color: var(--token-text); }
3250
+ .page-btn.is-active {
3251
+ background: var(--token-accent); color: var(--token-on-accent);
3252
+ font-weight: 500; box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
3253
+ }
3254
+ .page-btn.is-disabled, .page-btn:disabled { opacity: .3; pointer-events: none; }
3255
+
3256
+ .page-ellipsis {
3257
+ min-width: 28px; height: 28px; font-size: 12px;
3258
+ display: inline-flex; align-items: center; justify-content: center;
3259
+ color: var(--token-text-faint);
3260
+ }
3261
+
3262
+
2037
3263
  /* ── components/panel.css ── */
2038
3264
  /*
2039
3265
  * @icen.ai/ui — Component: panel(白卡片容器 + 标题栏)
@@ -2087,6 +3313,360 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
2087
3313
  }
2088
3314
 
2089
3315
 
3316
+ /* ── components/popover.css ── */
3317
+ /*
3318
+ * @icen.ai/ui — Component: popover(锚定浮层,与 behaviors/popover 配套)
3319
+ * 面板由 JS portal 到 document.body 并按 fixed 定位;data-side 由布局函数写入。
3320
+ */
3321
+
3322
+ .popover {
3323
+ position: fixed; z-index: var(--z-popover);
3324
+ padding: 8px;
3325
+ background: var(--token-card-strong); color: var(--token-text);
3326
+ border: 1px solid var(--token-outline);
3327
+ box-shadow: var(--token-shadow-overlay);
3328
+ border-radius: var(--radius-lg);
3329
+ animation: icen-popover-in 150ms var(--ease-out);
3330
+ }
3331
+ .popover[data-side='top'] { animation-name: icen-popover-in-top; }
3332
+
3333
+ /* 进场动画与 .menu 同参数:fade + scale(.95→1) + 按侧位移 8px */
3334
+ @keyframes icen-popover-in {
3335
+ from { opacity: 0; transform: scale(0.95) translateY(-8px); }
3336
+ to { opacity: 1; transform: scale(1) translateY(0); }
3337
+ }
3338
+ @keyframes icen-popover-in-top {
3339
+ from { opacity: 0; transform: scale(0.95) translateY(8px); }
3340
+ to { opacity: 1; transform: scale(1) translateY(0); }
3341
+ }
3342
+
3343
+
3344
+ /* ── components/segmented.css ── */
3345
+ /*
3346
+ * @icen.ai/ui — Component: segmented(分段控件 / 切换组 / 数字步进器 / OTP)
3347
+ * 规格源自 opengal primitives/input.tsx 的 SegmentedControl 与 advanced-input.tsx
3348
+ * 的 ToggleButtonGroup / NumberStepper / OtpInput。
3349
+ * segmented 与 toggle-group 是纯 CSS 状态组件(.is-active);stepper 由使用方接线;
3350
+ * otp 单元格交互见 behaviors/input.ts 的 initInput。
3351
+ */
3352
+
3353
+ /* ── 分段控件(排他单选) ── */
3354
+ .segmented {
3355
+ display: inline-flex; align-items: center; gap: 2px; padding: 2px;
3356
+ background: var(--token-panel);
3357
+ border: 1px solid var(--token-outline-soft);
3358
+ border-radius: var(--radius-lg);
3359
+ box-shadow: var(--token-shadow-inset);
3360
+ }
3361
+
3362
+ .segmented-item {
3363
+ display: flex; align-items: center; gap: 6px;
3364
+ padding: 6px 12px; font-size: 12px; font-weight: 500;
3365
+ border: none; background: transparent; cursor: pointer;
3366
+ border-radius: var(--radius-md);
3367
+ color: var(--token-text-muted);
3368
+ }
3369
+ .segmented-item:hover:not(.is-active) {
3370
+ color: var(--token-text); background: var(--token-surface-hover);
3371
+ }
3372
+ .segmented-item.is-active {
3373
+ background: var(--token-accent); color: var(--token-on-accent, #fff);
3374
+ box-shadow: var(--token-shadow-elevated);
3375
+ }
3376
+ .segmented-item:disabled { opacity: 0.4; cursor: not-allowed; }
3377
+
3378
+ /* 尺寸:容器修饰符驱动 item 内边距 / 字号 */
3379
+ .segmented--sm .segmented-item { padding: 4px 10px; font-size: 11px; }
3380
+
3381
+ /* ── 切换组(segmented 同族变体:更松的 padding、item 更小圆角、active 阴影更轻) ── */
3382
+ .toggle-group {
3383
+ display: inline-flex; align-items: center; gap: 4px; padding: 4px;
3384
+ background: var(--token-panel);
3385
+ border: 1px solid var(--token-outline-soft);
3386
+ border-radius: var(--radius-lg);
3387
+ }
3388
+
3389
+ .toggle-item {
3390
+ display: flex; align-items: center; gap: 6px;
3391
+ padding: 6px 12px; font-size: 12px; font-weight: 500;
3392
+ border: none; background: transparent; cursor: pointer;
3393
+ border-radius: var(--radius-xs);
3394
+ color: var(--token-text-muted);
3395
+ }
3396
+ .toggle-item:hover:not(.is-active) {
3397
+ color: var(--token-text); background: var(--token-control-hover);
3398
+ }
3399
+ .toggle-item.is-active {
3400
+ background: var(--token-accent); color: var(--token-on-accent, #fff);
3401
+ box-shadow: var(--token-shadow-card);
3402
+ }
3403
+ .toggle-item:disabled { opacity: 0.4; cursor: not-allowed; }
3404
+ .toggle-group--sm .toggle-item { padding: 4px 10px; font-size: 11px; }
3405
+
3406
+ /* ── 数字步进器 ── */
3407
+ .stepper {
3408
+ display: inline-flex; align-items: stretch;
3409
+ background: var(--token-control);
3410
+ border: 1px solid var(--token-outline-soft);
3411
+ border-radius: var(--radius-lg);
3412
+ overflow: hidden;
3413
+ }
3414
+
3415
+ .stepper-btn {
3416
+ display: inline-flex; align-items: center; justify-content: center;
3417
+ padding: 0 10px; border: none; background: transparent; cursor: pointer;
3418
+ color: var(--token-text-muted);
3419
+ transition: background-color var(--duration-normal) var(--ease-out),
3420
+ color var(--duration-normal) var(--ease-out);
3421
+ }
3422
+ .stepper-btn:hover { background: var(--token-control-hover); color: var(--token-text); }
3423
+ .stepper-btn:disabled { opacity: 0.4; cursor: not-allowed; }
3424
+
3425
+ .stepper-input {
3426
+ width: 56px; padding: 8px 0;
3427
+ text-align: center; font-variant-numeric: tabular-nums;
3428
+ background: transparent; outline: none; border: none;
3429
+ border-left: 1px solid var(--token-outline-soft);
3430
+ border-right: 1px solid var(--token-outline-soft);
3431
+ color: var(--token-text); font: inherit; font-size: 13px;
3432
+ }
3433
+ /* 隐藏原生 number 箭头(± 交给 .stepper-btn) */
3434
+ .stepper-input::-webkit-outer-spin-button,
3435
+ .stepper-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
3436
+ .stepper-input[type='number'] { -moz-appearance: textfield; appearance: textfield; }
3437
+
3438
+ /* ── OTP 验证码单元格 ── */
3439
+ .otp { display: inline-flex; align-items: center; gap: 8px; }
3440
+
3441
+ .otp-cell {
3442
+ width: 40px; height: 48px; padding: 0;
3443
+ text-align: center; font-size: 18px; font-weight: 600;
3444
+ color: var(--token-text);
3445
+ background: var(--token-control);
3446
+ border: 1px solid var(--token-outline-soft);
3447
+ border-radius: var(--radius-sm);
3448
+ outline: none;
3449
+ transition: border-color var(--duration-normal) var(--ease-out),
3450
+ box-shadow var(--duration-normal) var(--ease-out);
3451
+ }
3452
+ .otp-cell:focus-visible {
3453
+ border-color: var(--token-accent);
3454
+ box-shadow: 0 0 0 2px var(--token-accent-subtle);
3455
+ }
3456
+ .otp-cell:disabled { opacity: 0.4; cursor: not-allowed; }
3457
+
3458
+
3459
+ /* ── components/select.css ── */
3460
+ /*
3461
+ * @icen.ai/ui — Component: select(弹层选择器)
3462
+ * 规格源自 opengal primitives/input.tsx 的 Select;与 behaviors/select.ts 配套。
3463
+ *
3464
+ * 契约:
3465
+ * <div class="select" data-select>
3466
+ * <button class="select-trigger pressable focus-ring" aria-haspopup="listbox" aria-expanded="false">
3467
+ * <span class="select-leading">…</span> 可选前缀
3468
+ * <span class="select-value is-empty">占位</span>
3469
+ * <svg class="select-chevron">…</svg>
3470
+ * </button>
3471
+ * <div class="select-panel" hidden>
3472
+ * <button class="select-option" data-value="x">…</button>
3473
+ * </div>
3474
+ * <input type="hidden" data-select-value /> 可选,behavior 同步 value
3475
+ * </div>
3476
+ * 状态:容器 .is-open;选项 .is-focused / .is-selected(尾部 ✓)/ .is-disabled;
3477
+ * .select-value.is-empty = 未选占位(faint 色,behavior 选中后移除)。
3478
+ */
3479
+
3480
+ .select { position: relative; width: 100%; }
3481
+
3482
+ .select-trigger {
3483
+ display: flex; align-items: center; gap: 8px; width: 100%;
3484
+ height: 36px; padding: 0 12px; font-size: 13px; text-align: left;
3485
+ background: var(--token-control); color: var(--token-text);
3486
+ border: 1px solid var(--token-outline-soft);
3487
+ border-radius: var(--radius-lg);
3488
+ box-shadow: var(--token-shadow-inset);
3489
+ cursor: pointer;
3490
+ }
3491
+ .select-trigger:focus-visible { border-color: var(--token-accent); }
3492
+ .select-trigger:disabled { opacity: 0.4; cursor: not-allowed; }
3493
+
3494
+ .select-trigger--sm { height: 32px; padding: 0 10px; font-size: 12px; }
3495
+ .select-trigger--lg { height: 40px; padding: 0 14px; font-size: 14px; }
3496
+
3497
+ .select-leading {
3498
+ display: inline-flex; align-items: center; flex-shrink: 0;
3499
+ color: var(--token-text-faint);
3500
+ }
3501
+ .select-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3502
+ .select-value.is-empty { color: var(--token-text-faint); }
3503
+
3504
+ .select-chevron {
3505
+ width: 14px; height: 14px; flex-shrink: 0;
3506
+ color: var(--token-text-faint);
3507
+ transition: transform var(--duration-fast) var(--ease-out);
3508
+ }
3509
+ .select.is-open .select-chevron { transform: rotate(180deg); }
3510
+
3511
+ .select-panel {
3512
+ position: absolute; top: calc(100% + 4px); left: 0;
3513
+ min-width: 100%; max-height: 240px; overflow-y: auto; padding: 4px;
3514
+ background: var(--token-card-strong);
3515
+ border: 1px solid var(--token-outline);
3516
+ border-radius: var(--radius-lg);
3517
+ box-shadow: var(--token-shadow-overlay);
3518
+ z-index: var(--z-popover);
3519
+ transform-origin: top center;
3520
+ animation: select-panel-in var(--duration-fast) var(--ease-out);
3521
+ }
3522
+ @keyframes select-panel-in {
3523
+ from { opacity: 0; transform: scale(0.95); }
3524
+ to { opacity: 1; transform: scale(1); }
3525
+ }
3526
+
3527
+ .select-option {
3528
+ display: flex; align-items: center; gap: 8px; width: 100%;
3529
+ padding: 6px 10px; font-size: 12px; text-align: left;
3530
+ border: none; background: transparent; color: var(--token-text);
3531
+ border-radius: var(--radius-sm); cursor: pointer;
3532
+ transition: background-color var(--duration-fast) var(--ease-out),
3533
+ color var(--duration-fast) var(--ease-out);
3534
+ }
3535
+ .select-option.is-focused { background: var(--token-fill-2); }
3536
+ .select-option.is-selected { color: var(--token-accent); }
3537
+ .select-option.is-selected::after { content: '✓'; margin-left: auto; flex-shrink: 0; }
3538
+ .select-option.is-disabled { opacity: 0.4; cursor: not-allowed; }
3539
+
3540
+
3541
+ /* ── components/sidebar.css ── */
3542
+ /*
3543
+ * @icen.ai/ui — Component: sidebar(侧栏导航,分组折叠配套 behaviors/sidebar)
3544
+ * 用法:<aside class="sidebar sidebar--sticky">
3545
+ * <div class="sidebar-group is-open">
3546
+ * <button class="sidebar-group-title" aria-expanded="true">分组</button>
3547
+ * <div class="sidebar-group-panel"><div class="sidebar-group-panel-inner">
3548
+ * <a class="sidebar-item is-active" href="…">条目</a>
3549
+ * </div></div>
3550
+ * </div>
3551
+ * </aside>
3552
+ * 契约:.is-open 由 behaviors/sidebar 切换(同步 aria-expanded);
3553
+ * 展开动画 = grid-template-rows 0fr↔1fr(同 accordion 约定),内层 overflow:hidden。
3554
+ * 滚动条样式归 base.css 全局纪律,这里只声明 overflow-y。
3555
+ */
3556
+
3557
+ .sidebar { width: 260px; flex-shrink: 0; overflow-y: auto; padding-right: 4px; }
3558
+ .sidebar--sticky { position: sticky; top: 80px; max-height: calc(100vh - 100px); }
3559
+
3560
+ .sidebar-group { margin-bottom: 8px; }
3561
+ .sidebar-group-title {
3562
+ width: 100%; display: flex; align-items: center; justify-content: space-between;
3563
+ padding: 8px 12px; border-radius: var(--radius-md);
3564
+ font-family: inherit; font-size: 11px; font-weight: 600;
3565
+ text-transform: uppercase; letter-spacing: 1px;
3566
+ color: var(--token-text-muted);
3567
+ background: none; border: 0; cursor: pointer;
3568
+ }
3569
+ .sidebar-group-title:hover { background: var(--token-control-hover); color: var(--token-text); }
3570
+ /* chevron:闭合态 rotate(-90deg),展开回正 */
3571
+ .sidebar-group-title::after {
3572
+ content: '▾'; font-size: 12px; line-height: 1; color: var(--token-text-muted);
3573
+ transform: rotate(-90deg);
3574
+ transition: transform var(--duration-normal) var(--ease-out);
3575
+ }
3576
+ .sidebar-group.is-open .sidebar-group-title::after { transform: none; }
3577
+
3578
+ .sidebar-group-panel {
3579
+ display: grid; grid-template-rows: 0fr;
3580
+ transition: grid-template-rows var(--duration-normal) var(--ease-out);
3581
+ }
3582
+ .sidebar-group.is-open .sidebar-group-panel { grid-template-rows: 1fr; }
3583
+ .sidebar-group-panel-inner { overflow: hidden; min-height: 0; }
3584
+
3585
+ .sidebar-item {
3586
+ width: 100%; display: flex; align-items: center; gap: 8px;
3587
+ padding: 8px 12px; border-radius: var(--radius-lg);
3588
+ font-size: 13px; color: var(--token-text); text-align: left;
3589
+ text-decoration: none; background: none; border: 0; cursor: pointer;
3590
+ }
3591
+ .sidebar-item:hover { background: var(--token-control-hover); text-decoration: none; }
3592
+ .sidebar-item.is-active { background: var(--token-accent-subtle); color: var(--token-accent); font-weight: 500; }
3593
+ .sidebar-item > svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--token-text-muted); }
3594
+ .sidebar-item.is-active > svg { color: var(--token-accent); }
3595
+ .sidebar-item > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3596
+
3597
+
3598
+ /* ── components/slider.css ── */
3599
+ /*
3600
+ * @icen.ai/ui — Component: slider(单值 / 双滑块)
3601
+ * 规格源自 opengal primitives/input.tsx 的 Slider 与 advanced-input.tsx 的 DualRangeSlider;
3602
+ * 与 behaviors/slider.ts 配套(透明原生 range 覆盖层驱动 fill/thumb 定位)。
3603
+ *
3604
+ * 契约(单值):
3605
+ * <div class="slider">
3606
+ * <div class="slider-track"></div>
3607
+ * <div class="slider-fill" style="width:40%"></div>
3608
+ * <div class="slider-thumb" style="left:calc(40% - 8px)"></div>
3609
+ * <input class="slider-native" type="range" />
3610
+ * </div>
3611
+ * 双滑块:.slider--dual 内 .slider-range(left/right 定位选中段)
3612
+ * + .slider-thumb--lo / .slider-thumb--hi + 两个 .slider-native[data-thumb=lo|hi]
3613
+ * label 行:.slider-row(label + 当前值,tabular-nums)
3614
+ */
3615
+
3616
+ .slider { position: relative; height: 20px; width: 100%; }
3617
+
3618
+ .slider-track {
3619
+ position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
3620
+ height: 6px; border-radius: var(--radius-full);
3621
+ background: color-mix(in srgb, var(--token-line-soft) 50%, transparent);
3622
+ }
3623
+
3624
+ .slider-fill {
3625
+ position: absolute; left: 0; top: 50%; transform: translateY(-50%);
3626
+ height: 6px; border-radius: var(--radius-full);
3627
+ background: var(--token-accent);
3628
+ transition: width var(--duration-normal) var(--ease-out);
3629
+ }
3630
+
3631
+ /* 双滑块选中段:left = lo%、right = (100 - hi)% */
3632
+ .slider-range {
3633
+ position: absolute; top: 50%; transform: translateY(-50%);
3634
+ height: 6px; border-radius: var(--radius-full);
3635
+ background: var(--token-accent);
3636
+ transition: left var(--duration-normal) var(--ease-out),
3637
+ right var(--duration-normal) var(--ease-out);
3638
+ }
3639
+
3640
+ .slider-thumb {
3641
+ position: absolute; top: 50%; transform: translateY(-50%);
3642
+ width: 16px; height: 16px; border-radius: var(--radius-full);
3643
+ background: var(--token-card);
3644
+ border: 2px solid var(--token-accent);
3645
+ box-shadow: var(--token-shadow-card);
3646
+ pointer-events: none;
3647
+ transition: transform var(--duration-fast) var(--ease-out),
3648
+ box-shadow var(--duration-fast) var(--ease-out);
3649
+ }
3650
+ /* 原生 input 透明,焦点环镜像到可见 thumb 上 */
3651
+ .slider:focus-within .slider-thumb {
3652
+ transform: translateY(-50%) scale(1.1);
3653
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--token-accent) 50%, transparent),
3654
+ 0 0 0 3px var(--token-bg);
3655
+ }
3656
+
3657
+ .slider-native {
3658
+ position: absolute; inset: 0; width: 100%; height: 100%;
3659
+ margin: 0; opacity: 0; cursor: pointer; z-index: 1;
3660
+ }
3661
+ .slider-native:disabled { cursor: not-allowed; }
3662
+
3663
+ .slider-row {
3664
+ display: flex; align-items: center; justify-content: space-between;
3665
+ font-size: 12px; color: var(--token-text-muted);
3666
+ font-variant-numeric: tabular-nums;
3667
+ }
3668
+
3669
+
2090
3670
  /* ── components/stat.css ── */
2091
3671
  /*
2092
3672
  * @icen.ai/ui — Component: stat(指标卡)
@@ -2108,6 +3688,168 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
2108
3688
  .stat-label { color: var(--token-text-faint); font-size: 11px; letter-spacing: 1px; margin-top: 2px; }
2109
3689
 
2110
3690
 
3691
+ /* ── components/steps.css ── */
3692
+ /*
3693
+ * @icen.ai/ui — Component: steps(步骤条,横向默认 / .steps--vertical 垂直)
3694
+ * 用法:<ol class="steps">
3695
+ * <li class="step step--finish">
3696
+ * <span class="step-line"></span>
3697
+ * <span class="step-dot">✓</span>
3698
+ * <div class="step-body">
3699
+ * <div class="step-title">已完成</div>
3700
+ * <div class="step-desc">描述</div>
3701
+ * </div>
3702
+ * </li>
3703
+ * </ol>
3704
+ * 三态:.step--finish(accent 实底,内容放 ✓)/ .step--process(accent 描边)/ .step--wait(灰)。
3705
+ * 连接线画在「当前步 → 下一步」之间,颜色跟随当前步状态;末步不显示(.step:last-child)。
3706
+ * 垂直版同一 DOM:.step-line 改为绝对左侧竖线,穿过 16px 间隙接到下一颗圆点。
3707
+ */
3708
+
3709
+ .steps { display: flex; list-style: none; margin: 0; padding: 0; }
3710
+ .step { flex: 1; position: relative; text-align: center; }
3711
+
3712
+ .step-line {
3713
+ position: absolute; top: 16px; left: calc(50% + 20px);
3714
+ width: calc(100% - 40px); height: 1px;
3715
+ background: var(--token-outline-soft);
3716
+ }
3717
+ .step:last-child .step-line { display: none; }
3718
+ .step--finish .step-line { background: color-mix(in srgb, var(--token-accent) 40%, transparent); }
3719
+
3720
+ .step-dot {
3721
+ width: 32px; height: 32px; border-radius: var(--radius-full);
3722
+ display: inline-flex; align-items: center; justify-content: center;
3723
+ font-size: 12px; font-weight: 600; position: relative; z-index: 1;
3724
+ }
3725
+ .step--finish .step-dot { background: var(--token-accent); color: var(--token-on-accent); }
3726
+ .step--process .step-dot {
3727
+ background: var(--token-accent-subtle); color: var(--token-accent);
3728
+ border: 2px solid var(--token-accent);
3729
+ }
3730
+ .step--wait .step-dot {
3731
+ background: var(--token-control); color: var(--token-text-faint);
3732
+ border: 1px solid var(--token-outline-soft);
3733
+ }
3734
+
3735
+ .step-title { font-size: 13px; font-weight: 500; margin-top: 6px; }
3736
+ .step--wait .step-title { color: var(--token-text-muted); }
3737
+ .step-desc {
3738
+ font-size: 11px; color: var(--token-text-faint);
3739
+ margin-top: 2px; max-width: 160px; margin-inline: auto;
3740
+ }
3741
+
3742
+ /* ── 垂直变体 ── */
3743
+ .steps--vertical { flex-direction: column; gap: 16px; }
3744
+ .steps--vertical .step { flex: none; display: flex; gap: 12px; text-align: left; }
3745
+ .steps--vertical .step-line {
3746
+ top: 36px; bottom: -16px; left: 16px;
3747
+ width: 1px; height: auto;
3748
+ }
3749
+ .steps--vertical .step-dot { flex-shrink: 0; }
3750
+ .steps--vertical .step-body { padding-top: 4px; }
3751
+ .steps--vertical .step-title { margin-top: 0; }
3752
+ .steps--vertical .step-desc { max-width: none; margin-inline: 0; }
3753
+
3754
+
3755
+ /* ── components/switch.css ── */
3756
+ /*
3757
+ * @icen.ai/ui — Component: switch(开关 / 勾选框 / 单选行)
3758
+ * 纯 CSS 状态组件:状态全走 aria-checked / .is-selected,交互由使用方(或演示页脚本)切换。
3759
+ *
3760
+ * 开关:<button class="switch [--sm]" role="switch" aria-checked="true"><span class="switch-thumb"></span></button>
3761
+ * 勾选:<button class="checkbox focus-ring" role="checkbox" aria-checked="true|false|mixed"></button>
3762
+ * 单选:<div class="radio-group" role="radiogroup">
3763
+ * <button class="radio-item motion is-selected"><span class="radio-dot"></span>…</button>
3764
+ * </div>
3765
+ */
3766
+
3767
+ /* ── 开关 ── */
3768
+ .switch {
3769
+ position: relative; display: inline-flex; flex-shrink: 0;
3770
+ width: 44px; height: 24px; padding: 0;
3771
+ border: none; border-radius: var(--radius-full);
3772
+ background: var(--token-control-hover); cursor: pointer;
3773
+ transition: background-color var(--duration-normal) var(--ease-out);
3774
+ }
3775
+ .switch[aria-checked='true'] { background: var(--token-accent); }
3776
+ .switch:disabled { opacity: 0.4; cursor: not-allowed; }
3777
+
3778
+ .switch-thumb {
3779
+ position: absolute; top: 2px; left: 2px;
3780
+ width: 20px; height: 20px; border-radius: var(--radius-full);
3781
+ background: var(--token-on-accent, #fff);
3782
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
3783
+ transition: transform var(--duration-normal) var(--ease-out);
3784
+ }
3785
+ .switch[aria-checked='true'] .switch-thumb { transform: translateX(20px); }
3786
+
3787
+ .switch--sm { width: 36px; height: 20px; }
3788
+ .switch--sm .switch-thumb { width: 16px; height: 16px; }
3789
+ .switch--sm[aria-checked='true'] .switch-thumb { transform: translateX(16px); }
3790
+
3791
+ /* ── 勾选框(✓ 与半选横条均 ::before 自绘,无需内联 svg) ── */
3792
+ .checkbox {
3793
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
3794
+ width: 16px; height: 16px; padding: 0;
3795
+ background: var(--token-control); color: transparent;
3796
+ border: 1px solid var(--token-outline-soft);
3797
+ border-radius: var(--radius-sm); cursor: pointer;
3798
+ transition: background-color var(--duration-normal) var(--ease-out),
3799
+ border-color var(--duration-normal) var(--ease-out),
3800
+ color var(--duration-normal) var(--ease-out);
3801
+ }
3802
+ .checkbox:hover:not([aria-checked='true']):not([aria-checked='mixed']) {
3803
+ border-color: color-mix(in srgb, var(--token-accent) 50%, transparent);
3804
+ }
3805
+ .checkbox[aria-checked='true'], .checkbox[aria-checked='mixed'] {
3806
+ background: var(--token-accent);
3807
+ border-color: var(--token-accent);
3808
+ color: var(--token-on-accent, #fff);
3809
+ }
3810
+ .checkbox::before {
3811
+ content: ''; width: 10px; height: 10px;
3812
+ background: currentColor; opacity: 0;
3813
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
3814
+ }
3815
+ .checkbox[aria-checked='true']::before { opacity: 1; }
3816
+ .checkbox[aria-checked='mixed']::before {
3817
+ opacity: 1; width: 8px; height: 2px;
3818
+ clip-path: none; border-radius: var(--radius-full);
3819
+ }
3820
+ .checkbox:disabled { opacity: 0.4; cursor: not-allowed; }
3821
+
3822
+ /* ── 单选行 ── */
3823
+ .radio-group { display: flex; flex-direction: column; gap: 4px; }
3824
+
3825
+ .radio-item {
3826
+ display: flex; align-items: center; gap: 8px; width: 100%;
3827
+ padding: 8px 12px; font-size: 13px; text-align: left;
3828
+ border: none; background: transparent; color: var(--token-text);
3829
+ border-radius: var(--radius-sm); cursor: pointer;
3830
+ }
3831
+ .radio-item.is-selected {
3832
+ background: var(--token-accent-subtle); color: var(--token-accent); font-weight: 500;
3833
+ }
3834
+ .radio-item:disabled { opacity: 0.4; cursor: not-allowed; }
3835
+
3836
+ .radio-dot {
3837
+ position: relative; width: 16px; height: 16px; flex-shrink: 0;
3838
+ border: 1px solid var(--token-outline-soft);
3839
+ border-radius: var(--radius-full);
3840
+ transition: border-color var(--duration-normal) var(--ease-out);
3841
+ }
3842
+ .radio-item.is-selected .radio-dot { border-color: var(--token-accent); }
3843
+ .radio-dot::after {
3844
+ content: ''; position: absolute; top: 50%; left: 50%;
3845
+ width: 8px; height: 8px; border-radius: var(--radius-full);
3846
+ background: var(--token-accent);
3847
+ transform: translate(-50%, -50%) scale(0);
3848
+ transition: transform var(--duration-fast) var(--ease-out);
3849
+ }
3850
+ .radio-item.is-selected .radio-dot::after { transform: translate(-50%, -50%) scale(1); }
3851
+
3852
+
2111
3853
  /* ── components/table.css ── */
2112
3854
  /*
2113
3855
  * @icen.ai/ui — Component: table(数据表)
@@ -2159,6 +3901,57 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
2159
3901
  .admin-tabs a.active { color: var(--token-accent); border-bottom-color: var(--token-accent); }
2160
3902
 
2161
3903
 
3904
+ /* ── components/tag-input.css ── */
3905
+ /*
3906
+ * @icen.ai/ui — Component: tag-input(多值标签编辑)
3907
+ * 规格源自 opengal primitives/advanced-input.tsx 的 TagInput;与 behaviors/tag-input.ts 配套。
3908
+ *
3909
+ * 契约:
3910
+ * <div class="tag-input control" data-tags="初始,标签" data-max="5">
3911
+ * <input class="tag-input-field" placeholder="…" />
3912
+ * </div>
3913
+ * behavior 按 data-tags 渲染 .tag-chip(含 .tag-chip-x 删除钮),并回写 data-tags;
3914
+ * 达到 data-max 时容器加 .is-max(warning 色边框)并拒加。
3915
+ */
3916
+
3917
+ .tag-input {
3918
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
3919
+ padding: 6px; min-height: 36px;
3920
+ background: var(--token-control);
3921
+ border: 1px solid var(--token-outline-soft);
3922
+ border-radius: var(--radius-lg);
3923
+ }
3924
+ .tag-input:focus-within { border-color: var(--token-accent); }
3925
+ .tag-input.is-max, .tag-input.is-max:focus-within {
3926
+ border-color: color-mix(in srgb, var(--token-warning) 60%, transparent);
3927
+ }
3928
+ .tag-input.is-disabled { opacity: 0.4; }
3929
+ .tag-input.is-disabled .tag-input-field { cursor: not-allowed; }
3930
+
3931
+ .tag-chip {
3932
+ display: inline-flex; align-items: center; gap: 4px;
3933
+ padding: 2px 4px 2px 8px;
3934
+ border-radius: var(--radius-full);
3935
+ background: var(--token-accent-subtle); color: var(--token-accent);
3936
+ font-size: 11px; font-weight: 500;
3937
+ }
3938
+
3939
+ .tag-chip-x {
3940
+ display: inline-flex; align-items: center; justify-content: center;
3941
+ padding: 2px; border: none; background: transparent; cursor: pointer;
3942
+ border-radius: var(--radius-full); color: inherit; font-size: 11px; line-height: 1;
3943
+ transition: background-color var(--duration-normal) var(--ease-out);
3944
+ }
3945
+ .tag-chip-x:hover { background: var(--token-fill-2); }
3946
+
3947
+ .tag-input-field {
3948
+ flex: 1; min-width: 80px; padding: 4px;
3949
+ background: transparent; border: none; outline: none;
3950
+ color: var(--token-text); font: inherit; font-size: 12px;
3951
+ }
3952
+ .tag-input-field::placeholder { color: color-mix(in srgb, var(--token-text-faint) 60%, transparent); }
3953
+
3954
+
2162
3955
  /* ── components/toast.css ── */
2163
3956
  /*
2164
3957
  * @icen.ai/ui — Component: toast(轻提示,与 behaviors/toast 配套)
@@ -2166,7 +3959,7 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
2166
3959
  */
2167
3960
 
2168
3961
  .toast-container {
2169
- position: fixed; right: 20px; bottom: 20px; z-index: 9999;
3962
+ position: fixed; right: 20px; bottom: 20px; z-index: var(--z-toast);
2170
3963
  display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
2171
3964
  pointer-events: none;
2172
3965
  }
@@ -2188,3 +3981,108 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
2188
3981
  to { opacity: 1; transform: translateY(0); }
2189
3982
  }
2190
3983
 
3984
+
3985
+ /* ── components/tooltip.css ── */
3986
+ /*
3987
+ * @icen.ai/ui — Component: tooltip(纯 CSS 气泡提示,无 JS)
3988
+ * 契约:任意元素加 data-tooltip="文案";默认在上方,
3989
+ * data-tooltip-side="top|bottom|left|right" 换方向。
3990
+ * hover 延迟 400ms 出现,focus-visible 立即出现。
3991
+ */
3992
+
3993
+ [data-tooltip] { position: relative; }
3994
+
3995
+ [data-tooltip]::after {
3996
+ content: attr(data-tooltip);
3997
+ position: absolute; z-index: var(--z-tooltip);
3998
+ width: max-content; max-width: min(260px, calc(100vw - 32px));
3999
+ padding: 4px 8px;
4000
+ font-size: 11px; font-weight: 400; line-height: 1.375; white-space: normal; text-align: left;
4001
+ background: var(--token-card-strong); color: var(--token-text);
4002
+ border: 1px solid var(--token-outline-soft);
4003
+ box-shadow: var(--token-shadow-overlay);
4004
+ border-radius: var(--radius-md);
4005
+ opacity: 0; pointer-events: none;
4006
+ transition: opacity 150ms var(--ease-out), transform 150ms var(--ease-out);
4007
+ transition-delay: 400ms;
4008
+
4009
+ /* 默认:上方 */
4010
+ bottom: calc(100% + 4px); left: 50%;
4011
+ transform: translate(-50%, 4px);
4012
+ }
4013
+
4014
+ [data-tooltip-side='bottom']::after {
4015
+ bottom: auto; top: calc(100% + 4px); left: 50%;
4016
+ transform: translate(-50%, -4px);
4017
+ }
4018
+ [data-tooltip-side='left']::after {
4019
+ bottom: auto; top: 50%; left: auto; right: calc(100% + 4px);
4020
+ transform: translate(4px, -50%);
4021
+ }
4022
+ [data-tooltip-side='right']::after {
4023
+ bottom: auto; top: 50%; right: auto; left: calc(100% + 4px);
4024
+ transform: translate(-4px, -50%);
4025
+ }
4026
+
4027
+ [data-tooltip]:hover::after, [data-tooltip]:focus-visible::after {
4028
+ opacity: 1; transform: translate(-50%, 0);
4029
+ }
4030
+ [data-tooltip-side='bottom']:hover::after, [data-tooltip-side='bottom']:focus-visible::after {
4031
+ opacity: 1; transform: translate(-50%, 0);
4032
+ }
4033
+ [data-tooltip-side='left']:hover::after, [data-tooltip-side='left']:focus-visible::after {
4034
+ opacity: 1; transform: translate(0, -50%);
4035
+ }
4036
+ [data-tooltip-side='right']:hover::after, [data-tooltip-side='right']:focus-visible::after {
4037
+ opacity: 1; transform: translate(0, -50%);
4038
+ }
4039
+
4040
+ /* 键盘聚焦立即出现(覆盖 400ms 延迟) */
4041
+ [data-tooltip]:focus-visible::after { transition-delay: 0ms; }
4042
+
4043
+
4044
+ /* ── components/upload.css ── */
4045
+ /*
4046
+ * @icen.ai/ui — Component: upload(拖放上传区)
4047
+ * 规格源自 opengal primitives/advanced-input.tsx 的 Upload;与 behaviors/upload.ts 配套。
4048
+ *
4049
+ * 契约:
4050
+ * <div class="upload control lift focus-ring" role="button" tabindex="0">
4051
+ * <input type="file" multiple />
4052
+ * <div class="upload-icon"><svg …></svg></div>
4053
+ * <p class="upload-title">…</p>
4054
+ * <p class="upload-desc">…</p>
4055
+ * <p class="upload-error" hidden>…</p>
4056
+ * </div>
4057
+ * 拖入时 behavior 加 .is-drag。
4058
+ */
4059
+
4060
+ .upload {
4061
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
4062
+ text-align: center; padding: 32px 16px;
4063
+ border: 2px dashed var(--token-outline-soft);
4064
+ border-radius: var(--radius-lg);
4065
+ cursor: pointer;
4066
+ }
4067
+ .upload:hover {
4068
+ border-color: color-mix(in srgb, var(--token-accent) 60%, transparent);
4069
+ background: var(--token-control-hover);
4070
+ }
4071
+ .upload.is-drag {
4072
+ border-color: var(--token-accent);
4073
+ background: var(--token-accent-subtle);
4074
+ }
4075
+
4076
+ .upload input[type='file'] { display: none; }
4077
+
4078
+ .upload-icon {
4079
+ display: flex; align-items: center; justify-content: center;
4080
+ width: 40px; height: 40px; margin-bottom: 8px;
4081
+ border-radius: var(--radius-lg);
4082
+ background: var(--token-accent-subtle); color: var(--token-accent);
4083
+ }
4084
+
4085
+ .upload-title { margin: 0; font-size: 13px; font-weight: 500; color: var(--token-text); }
4086
+ .upload-desc { margin: 2px 0 0; font-size: 11px; color: var(--token-text-faint); }
4087
+ .upload-error { margin: 4px 0 0; font-size: 11px; color: var(--token-error); }
4088
+