@icen.ai/ui 0.1.0 → 0.2.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 (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
@@ -0,0 +1,32 @@
1
+ /*
2
+ * @icen.ai/ui — Component: pagination(分页)
3
+ * 用法:<nav class="pagination" aria-label="分页">
4
+ * <button class="page-btn motion is-disabled">‹</button>
5
+ * <button class="page-btn motion">1</button>
6
+ * <span class="page-ellipsis">…</span>
7
+ * <button class="page-btn motion is-active" aria-current="page">5</button>
8
+ * <button class="page-btn motion">›</button>
9
+ * </nav>
10
+ * 翻页逻辑由消费方实现,这里只提供静态样式契约。
11
+ */
12
+
13
+ .pagination { display: flex; align-items: center; gap: 4px; }
14
+
15
+ .page-btn {
16
+ min-width: 28px; height: 28px; padding: 0 6px;
17
+ border-radius: var(--radius-md); font-size: 12px;
18
+ display: inline-flex; align-items: center; justify-content: center;
19
+ color: var(--token-text-muted); background: none; border: 0; cursor: pointer;
20
+ }
21
+ .page-btn:not(.is-active):hover { background: var(--token-control-hover); color: var(--token-text); }
22
+ .page-btn.is-active {
23
+ background: var(--token-accent); color: var(--token-on-accent);
24
+ font-weight: 500; box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
25
+ }
26
+ .page-btn.is-disabled, .page-btn:disabled { opacity: .3; pointer-events: none; }
27
+
28
+ .page-ellipsis {
29
+ min-width: 28px; height: 28px; font-size: 12px;
30
+ display: inline-flex; align-items: center; justify-content: center;
31
+ color: var(--token-text-faint);
32
+ }
@@ -0,0 +1,25 @@
1
+ /*
2
+ * @icen.ai/ui — Component: popover(锚定浮层,与 behaviors/popover 配套)
3
+ * 面板由 JS portal 到 document.body 并按 fixed 定位;data-side 由布局函数写入。
4
+ */
5
+
6
+ .popover {
7
+ position: fixed; z-index: var(--z-popover);
8
+ padding: 8px;
9
+ background: var(--token-card-strong); color: var(--token-text);
10
+ border: 1px solid var(--token-outline);
11
+ box-shadow: var(--token-shadow-overlay);
12
+ border-radius: var(--radius-lg);
13
+ animation: icen-popover-in 150ms var(--ease-out);
14
+ }
15
+ .popover[data-side='top'] { animation-name: icen-popover-in-top; }
16
+
17
+ /* 进场动画与 .menu 同参数:fade + scale(.95→1) + 按侧位移 8px */
18
+ @keyframes icen-popover-in {
19
+ from { opacity: 0; transform: scale(0.95) translateY(-8px); }
20
+ to { opacity: 1; transform: scale(1) translateY(0); }
21
+ }
22
+ @keyframes icen-popover-in-top {
23
+ from { opacity: 0; transform: scale(0.95) translateY(8px); }
24
+ to { opacity: 1; transform: scale(1) translateY(0); }
25
+ }
@@ -0,0 +1,112 @@
1
+ /*
2
+ * @icen.ai/ui — Component: segmented(分段控件 / 切换组 / 数字步进器 / OTP)
3
+ * 规格源自 opengal primitives/input.tsx 的 SegmentedControl 与 advanced-input.tsx
4
+ * 的 ToggleButtonGroup / NumberStepper / OtpInput。
5
+ * segmented 与 toggle-group 是纯 CSS 状态组件(.is-active);stepper 由使用方接线;
6
+ * otp 单元格交互见 behaviors/input.ts 的 initInput。
7
+ */
8
+
9
+ /* ── 分段控件(排他单选) ── */
10
+ .segmented {
11
+ display: inline-flex; align-items: center; gap: 2px; padding: 2px;
12
+ background: var(--token-panel);
13
+ border: 1px solid var(--token-outline-soft);
14
+ border-radius: var(--radius-lg);
15
+ box-shadow: var(--token-shadow-inset);
16
+ }
17
+
18
+ .segmented-item {
19
+ display: flex; align-items: center; gap: 6px;
20
+ padding: 6px 12px; font-size: 12px; font-weight: 500;
21
+ border: none; background: transparent; cursor: pointer;
22
+ border-radius: var(--radius-md);
23
+ color: var(--token-text-muted);
24
+ }
25
+ .segmented-item:hover:not(.is-active) {
26
+ color: var(--token-text); background: var(--token-surface-hover);
27
+ }
28
+ .segmented-item.is-active {
29
+ background: var(--token-accent); color: var(--token-on-accent, #fff);
30
+ box-shadow: var(--token-shadow-elevated);
31
+ }
32
+ .segmented-item:disabled { opacity: 0.4; cursor: not-allowed; }
33
+
34
+ /* 尺寸:容器修饰符驱动 item 内边距 / 字号 */
35
+ .segmented--sm .segmented-item { padding: 4px 10px; font-size: 11px; }
36
+
37
+ /* ── 切换组(segmented 同族变体:更松的 padding、item 更小圆角、active 阴影更轻) ── */
38
+ .toggle-group {
39
+ display: inline-flex; align-items: center; gap: 4px; padding: 4px;
40
+ background: var(--token-panel);
41
+ border: 1px solid var(--token-outline-soft);
42
+ border-radius: var(--radius-lg);
43
+ }
44
+
45
+ .toggle-item {
46
+ display: flex; align-items: center; gap: 6px;
47
+ padding: 6px 12px; font-size: 12px; font-weight: 500;
48
+ border: none; background: transparent; cursor: pointer;
49
+ border-radius: var(--radius-xs);
50
+ color: var(--token-text-muted);
51
+ }
52
+ .toggle-item:hover:not(.is-active) {
53
+ color: var(--token-text); background: var(--token-control-hover);
54
+ }
55
+ .toggle-item.is-active {
56
+ background: var(--token-accent); color: var(--token-on-accent, #fff);
57
+ box-shadow: var(--token-shadow-card);
58
+ }
59
+ .toggle-item:disabled { opacity: 0.4; cursor: not-allowed; }
60
+ .toggle-group--sm .toggle-item { padding: 4px 10px; font-size: 11px; }
61
+
62
+ /* ── 数字步进器 ── */
63
+ .stepper {
64
+ display: inline-flex; align-items: stretch;
65
+ background: var(--token-control);
66
+ border: 1px solid var(--token-outline-soft);
67
+ border-radius: var(--radius-lg);
68
+ overflow: hidden;
69
+ }
70
+
71
+ .stepper-btn {
72
+ display: inline-flex; align-items: center; justify-content: center;
73
+ padding: 0 10px; border: none; background: transparent; cursor: pointer;
74
+ color: var(--token-text-muted);
75
+ transition: background-color var(--duration-normal) var(--ease-out),
76
+ color var(--duration-normal) var(--ease-out);
77
+ }
78
+ .stepper-btn:hover { background: var(--token-control-hover); color: var(--token-text); }
79
+ .stepper-btn:disabled { opacity: 0.4; cursor: not-allowed; }
80
+
81
+ .stepper-input {
82
+ width: 56px; padding: 8px 0;
83
+ text-align: center; font-variant-numeric: tabular-nums;
84
+ background: transparent; outline: none; border: none;
85
+ border-left: 1px solid var(--token-outline-soft);
86
+ border-right: 1px solid var(--token-outline-soft);
87
+ color: var(--token-text); font: inherit; font-size: 13px;
88
+ }
89
+ /* 隐藏原生 number 箭头(± 交给 .stepper-btn) */
90
+ .stepper-input::-webkit-outer-spin-button,
91
+ .stepper-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
92
+ .stepper-input[type='number'] { -moz-appearance: textfield; appearance: textfield; }
93
+
94
+ /* ── OTP 验证码单元格 ── */
95
+ .otp { display: inline-flex; align-items: center; gap: 8px; }
96
+
97
+ .otp-cell {
98
+ width: 40px; height: 48px; padding: 0;
99
+ text-align: center; font-size: 18px; font-weight: 600;
100
+ color: var(--token-text);
101
+ background: var(--token-control);
102
+ border: 1px solid var(--token-outline-soft);
103
+ border-radius: var(--radius-sm);
104
+ outline: none;
105
+ transition: border-color var(--duration-normal) var(--ease-out),
106
+ box-shadow var(--duration-normal) var(--ease-out);
107
+ }
108
+ .otp-cell:focus-visible {
109
+ border-color: var(--token-accent);
110
+ box-shadow: 0 0 0 2px var(--token-accent-subtle);
111
+ }
112
+ .otp-cell:disabled { opacity: 0.4; cursor: not-allowed; }
@@ -0,0 +1,79 @@
1
+ /*
2
+ * @icen.ai/ui — Component: select(弹层选择器)
3
+ * 规格源自 opengal primitives/input.tsx 的 Select;与 behaviors/select.ts 配套。
4
+ *
5
+ * 契约:
6
+ * <div class="select" data-select>
7
+ * <button class="select-trigger pressable focus-ring" aria-haspopup="listbox" aria-expanded="false">
8
+ * <span class="select-leading">…</span> 可选前缀
9
+ * <span class="select-value is-empty">占位</span>
10
+ * <svg class="select-chevron">…</svg>
11
+ * </button>
12
+ * <div class="select-panel" hidden>
13
+ * <button class="select-option" data-value="x">…</button>
14
+ * </div>
15
+ * <input type="hidden" data-select-value /> 可选,behavior 同步 value
16
+ * </div>
17
+ * 状态:容器 .is-open;选项 .is-focused / .is-selected(尾部 ✓)/ .is-disabled;
18
+ * .select-value.is-empty = 未选占位(faint 色,behavior 选中后移除)。
19
+ */
20
+
21
+ .select { position: relative; width: 100%; }
22
+
23
+ .select-trigger {
24
+ display: flex; align-items: center; gap: 8px; width: 100%;
25
+ height: 36px; padding: 0 12px; font-size: 13px; text-align: left;
26
+ background: var(--token-control); color: var(--token-text);
27
+ border: 1px solid var(--token-outline-soft);
28
+ border-radius: var(--radius-lg);
29
+ box-shadow: var(--token-shadow-inset);
30
+ cursor: pointer;
31
+ }
32
+ .select-trigger:focus-visible { border-color: var(--token-accent); }
33
+ .select-trigger:disabled { opacity: 0.4; cursor: not-allowed; }
34
+
35
+ .select-trigger--sm { height: 32px; padding: 0 10px; font-size: 12px; }
36
+ .select-trigger--lg { height: 40px; padding: 0 14px; font-size: 14px; }
37
+
38
+ .select-leading {
39
+ display: inline-flex; align-items: center; flex-shrink: 0;
40
+ color: var(--token-text-faint);
41
+ }
42
+ .select-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
43
+ .select-value.is-empty { color: var(--token-text-faint); }
44
+
45
+ .select-chevron {
46
+ width: 14px; height: 14px; flex-shrink: 0;
47
+ color: var(--token-text-faint);
48
+ transition: transform var(--duration-fast) var(--ease-out);
49
+ }
50
+ .select.is-open .select-chevron { transform: rotate(180deg); }
51
+
52
+ .select-panel {
53
+ position: absolute; top: calc(100% + 4px); left: 0;
54
+ min-width: 100%; max-height: 240px; overflow-y: auto; padding: 4px;
55
+ background: var(--token-card-strong);
56
+ border: 1px solid var(--token-outline);
57
+ border-radius: var(--radius-lg);
58
+ box-shadow: var(--token-shadow-overlay);
59
+ z-index: var(--z-popover);
60
+ transform-origin: top center;
61
+ animation: select-panel-in var(--duration-fast) var(--ease-out);
62
+ }
63
+ @keyframes select-panel-in {
64
+ from { opacity: 0; transform: scale(0.95); }
65
+ to { opacity: 1; transform: scale(1); }
66
+ }
67
+
68
+ .select-option {
69
+ display: flex; align-items: center; gap: 8px; width: 100%;
70
+ padding: 6px 10px; font-size: 12px; text-align: left;
71
+ border: none; background: transparent; color: var(--token-text);
72
+ border-radius: var(--radius-sm); cursor: pointer;
73
+ transition: background-color var(--duration-fast) var(--ease-out),
74
+ color var(--duration-fast) var(--ease-out);
75
+ }
76
+ .select-option.is-focused { background: var(--token-fill-2); }
77
+ .select-option.is-selected { color: var(--token-accent); }
78
+ .select-option.is-selected::after { content: '✓'; margin-left: auto; flex-shrink: 0; }
79
+ .select-option.is-disabled { opacity: 0.4; cursor: not-allowed; }
@@ -0,0 +1,54 @@
1
+ /*
2
+ * @icen.ai/ui — Component: sidebar(侧栏导航,分组折叠配套 behaviors/sidebar)
3
+ * 用法:<aside class="sidebar sidebar--sticky">
4
+ * <div class="sidebar-group is-open">
5
+ * <button class="sidebar-group-title" aria-expanded="true">分组</button>
6
+ * <div class="sidebar-group-panel"><div class="sidebar-group-panel-inner">
7
+ * <a class="sidebar-item is-active" href="…">条目</a>
8
+ * </div></div>
9
+ * </div>
10
+ * </aside>
11
+ * 契约:.is-open 由 behaviors/sidebar 切换(同步 aria-expanded);
12
+ * 展开动画 = grid-template-rows 0fr↔1fr(同 accordion 约定),内层 overflow:hidden。
13
+ * 滚动条样式归 base.css 全局纪律,这里只声明 overflow-y。
14
+ */
15
+
16
+ .sidebar { width: 260px; flex-shrink: 0; overflow-y: auto; padding-right: 4px; }
17
+ .sidebar--sticky { position: sticky; top: 80px; max-height: calc(100vh - 100px); }
18
+
19
+ .sidebar-group { margin-bottom: 8px; }
20
+ .sidebar-group-title {
21
+ width: 100%; display: flex; align-items: center; justify-content: space-between;
22
+ padding: 8px 12px; border-radius: var(--radius-md);
23
+ font-family: inherit; font-size: 11px; font-weight: 600;
24
+ text-transform: uppercase; letter-spacing: 1px;
25
+ color: var(--token-text-muted);
26
+ background: none; border: 0; cursor: pointer;
27
+ }
28
+ .sidebar-group-title:hover { background: var(--token-control-hover); color: var(--token-text); }
29
+ /* chevron:闭合态 rotate(-90deg),展开回正 */
30
+ .sidebar-group-title::after {
31
+ content: '▾'; font-size: 12px; line-height: 1; color: var(--token-text-muted);
32
+ transform: rotate(-90deg);
33
+ transition: transform var(--duration-normal) var(--ease-out);
34
+ }
35
+ .sidebar-group.is-open .sidebar-group-title::after { transform: none; }
36
+
37
+ .sidebar-group-panel {
38
+ display: grid; grid-template-rows: 0fr;
39
+ transition: grid-template-rows var(--duration-normal) var(--ease-out);
40
+ }
41
+ .sidebar-group.is-open .sidebar-group-panel { grid-template-rows: 1fr; }
42
+ .sidebar-group-panel-inner { overflow: hidden; min-height: 0; }
43
+
44
+ .sidebar-item {
45
+ width: 100%; display: flex; align-items: center; gap: 8px;
46
+ padding: 8px 12px; border-radius: var(--radius-lg);
47
+ font-size: 13px; color: var(--token-text); text-align: left;
48
+ text-decoration: none; background: none; border: 0; cursor: pointer;
49
+ }
50
+ .sidebar-item:hover { background: var(--token-control-hover); text-decoration: none; }
51
+ .sidebar-item.is-active { background: var(--token-accent-subtle); color: var(--token-accent); font-weight: 500; }
52
+ .sidebar-item > svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--token-text-muted); }
53
+ .sidebar-item.is-active > svg { color: var(--token-accent); }
54
+ .sidebar-item > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * @icen.ai/ui — Component: slider(单值 / 双滑块)
3
+ * 规格源自 opengal primitives/input.tsx 的 Slider 与 advanced-input.tsx 的 DualRangeSlider;
4
+ * 与 behaviors/slider.ts 配套(透明原生 range 覆盖层驱动 fill/thumb 定位)。
5
+ *
6
+ * 契约(单值):
7
+ * <div class="slider">
8
+ * <div class="slider-track"></div>
9
+ * <div class="slider-fill" style="width:40%"></div>
10
+ * <div class="slider-thumb" style="left:calc(40% - 8px)"></div>
11
+ * <input class="slider-native" type="range" />
12
+ * </div>
13
+ * 双滑块:.slider--dual 内 .slider-range(left/right 定位选中段)
14
+ * + .slider-thumb--lo / .slider-thumb--hi + 两个 .slider-native[data-thumb=lo|hi]
15
+ * label 行:.slider-row(label + 当前值,tabular-nums)
16
+ */
17
+
18
+ .slider { position: relative; height: 20px; width: 100%; }
19
+
20
+ .slider-track {
21
+ position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
22
+ height: 6px; border-radius: var(--radius-full);
23
+ background: color-mix(in srgb, var(--token-line-soft) 50%, transparent);
24
+ }
25
+
26
+ .slider-fill {
27
+ position: absolute; left: 0; top: 50%; transform: translateY(-50%);
28
+ height: 6px; border-radius: var(--radius-full);
29
+ background: var(--token-accent);
30
+ transition: width var(--duration-normal) var(--ease-out);
31
+ }
32
+
33
+ /* 双滑块选中段:left = lo%、right = (100 - hi)% */
34
+ .slider-range {
35
+ position: absolute; top: 50%; transform: translateY(-50%);
36
+ height: 6px; border-radius: var(--radius-full);
37
+ background: var(--token-accent);
38
+ transition: left var(--duration-normal) var(--ease-out),
39
+ right var(--duration-normal) var(--ease-out);
40
+ }
41
+
42
+ .slider-thumb {
43
+ position: absolute; top: 50%; transform: translateY(-50%);
44
+ width: 16px; height: 16px; border-radius: var(--radius-full);
45
+ background: var(--token-card);
46
+ border: 2px solid var(--token-accent);
47
+ box-shadow: var(--token-shadow-card);
48
+ pointer-events: none;
49
+ transition: transform var(--duration-fast) var(--ease-out),
50
+ box-shadow var(--duration-fast) var(--ease-out);
51
+ }
52
+ /* 原生 input 透明,焦点环镜像到可见 thumb 上 */
53
+ .slider:focus-within .slider-thumb {
54
+ transform: translateY(-50%) scale(1.1);
55
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--token-accent) 50%, transparent),
56
+ 0 0 0 3px var(--token-bg);
57
+ }
58
+
59
+ .slider-native {
60
+ position: absolute; inset: 0; width: 100%; height: 100%;
61
+ margin: 0; opacity: 0; cursor: pointer; z-index: 1;
62
+ }
63
+ .slider-native:disabled { cursor: not-allowed; }
64
+
65
+ .slider-row {
66
+ display: flex; align-items: center; justify-content: space-between;
67
+ font-size: 12px; color: var(--token-text-muted);
68
+ font-variant-numeric: tabular-nums;
69
+ }
@@ -0,0 +1,61 @@
1
+ /*
2
+ * @icen.ai/ui — Component: steps(步骤条,横向默认 / .steps--vertical 垂直)
3
+ * 用法:<ol class="steps">
4
+ * <li class="step step--finish">
5
+ * <span class="step-line"></span>
6
+ * <span class="step-dot">✓</span>
7
+ * <div class="step-body">
8
+ * <div class="step-title">已完成</div>
9
+ * <div class="step-desc">描述</div>
10
+ * </div>
11
+ * </li>
12
+ * </ol>
13
+ * 三态:.step--finish(accent 实底,内容放 ✓)/ .step--process(accent 描边)/ .step--wait(灰)。
14
+ * 连接线画在「当前步 → 下一步」之间,颜色跟随当前步状态;末步不显示(.step:last-child)。
15
+ * 垂直版同一 DOM:.step-line 改为绝对左侧竖线,穿过 16px 间隙接到下一颗圆点。
16
+ */
17
+
18
+ .steps { display: flex; list-style: none; margin: 0; padding: 0; }
19
+ .step { flex: 1; position: relative; text-align: center; }
20
+
21
+ .step-line {
22
+ position: absolute; top: 16px; left: calc(50% + 20px);
23
+ width: calc(100% - 40px); height: 1px;
24
+ background: var(--token-outline-soft);
25
+ }
26
+ .step:last-child .step-line { display: none; }
27
+ .step--finish .step-line { background: color-mix(in srgb, var(--token-accent) 40%, transparent); }
28
+
29
+ .step-dot {
30
+ width: 32px; height: 32px; border-radius: var(--radius-full);
31
+ display: inline-flex; align-items: center; justify-content: center;
32
+ font-size: 12px; font-weight: 600; position: relative; z-index: 1;
33
+ }
34
+ .step--finish .step-dot { background: var(--token-accent); color: var(--token-on-accent); }
35
+ .step--process .step-dot {
36
+ background: var(--token-accent-subtle); color: var(--token-accent);
37
+ border: 2px solid var(--token-accent);
38
+ }
39
+ .step--wait .step-dot {
40
+ background: var(--token-control); color: var(--token-text-faint);
41
+ border: 1px solid var(--token-outline-soft);
42
+ }
43
+
44
+ .step-title { font-size: 13px; font-weight: 500; margin-top: 6px; }
45
+ .step--wait .step-title { color: var(--token-text-muted); }
46
+ .step-desc {
47
+ font-size: 11px; color: var(--token-text-faint);
48
+ margin-top: 2px; max-width: 160px; margin-inline: auto;
49
+ }
50
+
51
+ /* ── 垂直变体 ── */
52
+ .steps--vertical { flex-direction: column; gap: 16px; }
53
+ .steps--vertical .step { flex: none; display: flex; gap: 12px; text-align: left; }
54
+ .steps--vertical .step-line {
55
+ top: 36px; bottom: -16px; left: 16px;
56
+ width: 1px; height: auto;
57
+ }
58
+ .steps--vertical .step-dot { flex-shrink: 0; }
59
+ .steps--vertical .step-body { padding-top: 4px; }
60
+ .steps--vertical .step-title { margin-top: 0; }
61
+ .steps--vertical .step-desc { max-width: none; margin-inline: 0; }
@@ -0,0 +1,95 @@
1
+ /*
2
+ * @icen.ai/ui — Component: switch(开关 / 勾选框 / 单选行)
3
+ * 纯 CSS 状态组件:状态全走 aria-checked / .is-selected,交互由使用方(或演示页脚本)切换。
4
+ *
5
+ * 开关:<button class="switch [--sm]" role="switch" aria-checked="true"><span class="switch-thumb"></span></button>
6
+ * 勾选:<button class="checkbox focus-ring" role="checkbox" aria-checked="true|false|mixed"></button>
7
+ * 单选:<div class="radio-group" role="radiogroup">
8
+ * <button class="radio-item motion is-selected"><span class="radio-dot"></span>…</button>
9
+ * </div>
10
+ */
11
+
12
+ /* ── 开关 ── */
13
+ .switch {
14
+ position: relative; display: inline-flex; flex-shrink: 0;
15
+ width: 44px; height: 24px; padding: 0;
16
+ border: none; border-radius: var(--radius-full);
17
+ background: var(--token-control-hover); cursor: pointer;
18
+ transition: background-color var(--duration-normal) var(--ease-out);
19
+ }
20
+ .switch[aria-checked='true'] { background: var(--token-accent); }
21
+ .switch:disabled { opacity: 0.4; cursor: not-allowed; }
22
+
23
+ .switch-thumb {
24
+ position: absolute; top: 2px; left: 2px;
25
+ width: 20px; height: 20px; border-radius: var(--radius-full);
26
+ background: var(--token-on-accent, #fff);
27
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
28
+ transition: transform var(--duration-normal) var(--ease-out);
29
+ }
30
+ .switch[aria-checked='true'] .switch-thumb { transform: translateX(20px); }
31
+
32
+ .switch--sm { width: 36px; height: 20px; }
33
+ .switch--sm .switch-thumb { width: 16px; height: 16px; }
34
+ .switch--sm[aria-checked='true'] .switch-thumb { transform: translateX(16px); }
35
+
36
+ /* ── 勾选框(✓ 与半选横条均 ::before 自绘,无需内联 svg) ── */
37
+ .checkbox {
38
+ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
39
+ width: 16px; height: 16px; padding: 0;
40
+ background: var(--token-control); color: transparent;
41
+ border: 1px solid var(--token-outline-soft);
42
+ border-radius: var(--radius-sm); cursor: pointer;
43
+ transition: background-color var(--duration-normal) var(--ease-out),
44
+ border-color var(--duration-normal) var(--ease-out),
45
+ color var(--duration-normal) var(--ease-out);
46
+ }
47
+ .checkbox:hover:not([aria-checked='true']):not([aria-checked='mixed']) {
48
+ border-color: color-mix(in srgb, var(--token-accent) 50%, transparent);
49
+ }
50
+ .checkbox[aria-checked='true'], .checkbox[aria-checked='mixed'] {
51
+ background: var(--token-accent);
52
+ border-color: var(--token-accent);
53
+ color: var(--token-on-accent, #fff);
54
+ }
55
+ .checkbox::before {
56
+ content: ''; width: 10px; height: 10px;
57
+ background: currentColor; opacity: 0;
58
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
59
+ }
60
+ .checkbox[aria-checked='true']::before { opacity: 1; }
61
+ .checkbox[aria-checked='mixed']::before {
62
+ opacity: 1; width: 8px; height: 2px;
63
+ clip-path: none; border-radius: var(--radius-full);
64
+ }
65
+ .checkbox:disabled { opacity: 0.4; cursor: not-allowed; }
66
+
67
+ /* ── 单选行 ── */
68
+ .radio-group { display: flex; flex-direction: column; gap: 4px; }
69
+
70
+ .radio-item {
71
+ display: flex; align-items: center; gap: 8px; width: 100%;
72
+ padding: 8px 12px; font-size: 13px; text-align: left;
73
+ border: none; background: transparent; color: var(--token-text);
74
+ border-radius: var(--radius-sm); cursor: pointer;
75
+ }
76
+ .radio-item.is-selected {
77
+ background: var(--token-accent-subtle); color: var(--token-accent); font-weight: 500;
78
+ }
79
+ .radio-item:disabled { opacity: 0.4; cursor: not-allowed; }
80
+
81
+ .radio-dot {
82
+ position: relative; width: 16px; height: 16px; flex-shrink: 0;
83
+ border: 1px solid var(--token-outline-soft);
84
+ border-radius: var(--radius-full);
85
+ transition: border-color var(--duration-normal) var(--ease-out);
86
+ }
87
+ .radio-item.is-selected .radio-dot { border-color: var(--token-accent); }
88
+ .radio-dot::after {
89
+ content: ''; position: absolute; top: 50%; left: 50%;
90
+ width: 8px; height: 8px; border-radius: var(--radius-full);
91
+ background: var(--token-accent);
92
+ transform: translate(-50%, -50%) scale(0);
93
+ transition: transform var(--duration-fast) var(--ease-out);
94
+ }
95
+ .radio-item.is-selected .radio-dot::after { transform: translate(-50%, -50%) scale(1); }
@@ -0,0 +1,48 @@
1
+ /*
2
+ * @icen.ai/ui — Component: tag-input(多值标签编辑)
3
+ * 规格源自 opengal primitives/advanced-input.tsx 的 TagInput;与 behaviors/tag-input.ts 配套。
4
+ *
5
+ * 契约:
6
+ * <div class="tag-input control" data-tags="初始,标签" data-max="5">
7
+ * <input class="tag-input-field" placeholder="…" />
8
+ * </div>
9
+ * behavior 按 data-tags 渲染 .tag-chip(含 .tag-chip-x 删除钮),并回写 data-tags;
10
+ * 达到 data-max 时容器加 .is-max(warning 色边框)并拒加。
11
+ */
12
+
13
+ .tag-input {
14
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
15
+ padding: 6px; min-height: 36px;
16
+ background: var(--token-control);
17
+ border: 1px solid var(--token-outline-soft);
18
+ border-radius: var(--radius-lg);
19
+ }
20
+ .tag-input:focus-within { border-color: var(--token-accent); }
21
+ .tag-input.is-max, .tag-input.is-max:focus-within {
22
+ border-color: color-mix(in srgb, var(--token-warning) 60%, transparent);
23
+ }
24
+ .tag-input.is-disabled { opacity: 0.4; }
25
+ .tag-input.is-disabled .tag-input-field { cursor: not-allowed; }
26
+
27
+ .tag-chip {
28
+ display: inline-flex; align-items: center; gap: 4px;
29
+ padding: 2px 4px 2px 8px;
30
+ border-radius: var(--radius-full);
31
+ background: var(--token-accent-subtle); color: var(--token-accent);
32
+ font-size: 11px; font-weight: 500;
33
+ }
34
+
35
+ .tag-chip-x {
36
+ display: inline-flex; align-items: center; justify-content: center;
37
+ padding: 2px; border: none; background: transparent; cursor: pointer;
38
+ border-radius: var(--radius-full); color: inherit; font-size: 11px; line-height: 1;
39
+ transition: background-color var(--duration-normal) var(--ease-out);
40
+ }
41
+ .tag-chip-x:hover { background: var(--token-fill-2); }
42
+
43
+ .tag-input-field {
44
+ flex: 1; min-width: 80px; padding: 4px;
45
+ background: transparent; border: none; outline: none;
46
+ color: var(--token-text); font: inherit; font-size: 12px;
47
+ }
48
+ .tag-input-field::placeholder { color: color-mix(in srgb, var(--token-text-faint) 60%, transparent); }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  .toast-container {
7
- position: fixed; right: 20px; bottom: 20px; z-index: 9999;
7
+ position: fixed; right: 20px; bottom: 20px; z-index: var(--z-toast);
8
8
  display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
9
9
  pointer-events: none;
10
10
  }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * @icen.ai/ui — Component: tooltip(纯 CSS 气泡提示,无 JS)
3
+ * 契约:任意元素加 data-tooltip="文案";默认在上方,
4
+ * data-tooltip-side="top|bottom|left|right" 换方向。
5
+ * hover 延迟 400ms 出现,focus-visible 立即出现。
6
+ */
7
+
8
+ [data-tooltip] { position: relative; }
9
+
10
+ [data-tooltip]::after {
11
+ content: attr(data-tooltip);
12
+ position: absolute; z-index: var(--z-tooltip);
13
+ width: max-content; max-width: min(260px, calc(100vw - 32px));
14
+ padding: 4px 8px;
15
+ font-size: 11px; font-weight: 400; line-height: 1.375; white-space: normal; text-align: left;
16
+ background: var(--token-card-strong); color: var(--token-text);
17
+ border: 1px solid var(--token-outline-soft);
18
+ box-shadow: var(--token-shadow-overlay);
19
+ border-radius: var(--radius-md);
20
+ opacity: 0; pointer-events: none;
21
+ transition: opacity 150ms var(--ease-out), transform 150ms var(--ease-out);
22
+ transition-delay: 400ms;
23
+
24
+ /* 默认:上方 */
25
+ bottom: calc(100% + 4px); left: 50%;
26
+ transform: translate(-50%, 4px);
27
+ }
28
+
29
+ [data-tooltip-side='bottom']::after {
30
+ bottom: auto; top: calc(100% + 4px); left: 50%;
31
+ transform: translate(-50%, -4px);
32
+ }
33
+ [data-tooltip-side='left']::after {
34
+ bottom: auto; top: 50%; left: auto; right: calc(100% + 4px);
35
+ transform: translate(4px, -50%);
36
+ }
37
+ [data-tooltip-side='right']::after {
38
+ bottom: auto; top: 50%; right: auto; left: calc(100% + 4px);
39
+ transform: translate(-4px, -50%);
40
+ }
41
+
42
+ [data-tooltip]:hover::after, [data-tooltip]:focus-visible::after {
43
+ opacity: 1; transform: translate(-50%, 0);
44
+ }
45
+ [data-tooltip-side='bottom']:hover::after, [data-tooltip-side='bottom']:focus-visible::after {
46
+ opacity: 1; transform: translate(-50%, 0);
47
+ }
48
+ [data-tooltip-side='left']:hover::after, [data-tooltip-side='left']:focus-visible::after {
49
+ opacity: 1; transform: translate(0, -50%);
50
+ }
51
+ [data-tooltip-side='right']:hover::after, [data-tooltip-side='right']:focus-visible::after {
52
+ opacity: 1; transform: translate(0, -50%);
53
+ }
54
+
55
+ /* 键盘聚焦立即出现(覆盖 400ms 延迟) */
56
+ [data-tooltip]:focus-visible::after { transition-delay: 0ms; }