@icen.ai/ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +29 -16
  2. package/dist/base.css +389 -48
  3. package/dist/behaviors/accordion.mjs +20 -0
  4. package/dist/behaviors/carousel.mjs +30 -1
  5. package/dist/behaviors/charts.mjs +168 -0
  6. package/dist/behaviors/copy.mjs +4 -3
  7. package/dist/behaviors/datatable.mjs +748 -0
  8. package/dist/behaviors/input.mjs +20 -1
  9. package/dist/behaviors/modal.mjs +15 -1
  10. package/dist/behaviors/nav.mjs +31 -4
  11. package/dist/behaviors/sidebar.mjs +12 -0
  12. package/dist/behaviors/slider.mjs +35 -3
  13. package/dist/behaviors/tabs.mjs +100 -9
  14. package/dist/behaviors/tag-input.mjs +11 -0
  15. package/dist/behaviors/theme.mjs +26 -0
  16. package/dist/behaviors/toast.mjs +83 -15
  17. package/dist/behaviors/tree.mjs +57 -1
  18. package/dist/behaviors/upload.mjs +2 -3
  19. package/dist/cli.mjs +74 -0
  20. package/dist/components/accordion.mjs +3 -0
  21. package/dist/components/alert.mjs +2 -0
  22. package/dist/components/avatar.mjs +2 -0
  23. package/dist/components/breadcrumb.css +91 -18
  24. package/dist/components/breadcrumb.mjs +2 -0
  25. package/dist/components/btn.css +174 -14
  26. package/dist/components/btn.mjs +2 -0
  27. package/dist/components/card.css +118 -18
  28. package/dist/components/card.mjs +2 -0
  29. package/dist/components/carousel.mjs +3 -0
  30. package/dist/components/charts.css +225 -33
  31. package/dist/components/charts.mjs +3 -0
  32. package/dist/components/content.css +256 -74
  33. package/dist/components/context-menu.mjs +3 -0
  34. package/dist/components/datatable.css +343 -0
  35. package/dist/components/datatable.mjs +4 -0
  36. package/dist/components/desc.mjs +2 -0
  37. package/dist/components/dropdown.mjs +3 -0
  38. package/dist/components/empty.css +50 -10
  39. package/dist/components/empty.mjs +2 -0
  40. package/dist/components/feedback.css +158 -47
  41. package/dist/components/form.css +139 -22
  42. package/dist/components/input.css +165 -45
  43. package/dist/components/input.mjs +3 -0
  44. package/dist/components/kbd.mjs +2 -0
  45. package/dist/components/layout.mjs +2 -0
  46. package/dist/components/list.mjs +2 -0
  47. package/dist/components/media-card.mjs +2 -0
  48. package/dist/components/media.css +187 -40
  49. package/dist/components/menu.css +159 -32
  50. package/dist/components/modal.css +140 -34
  51. package/dist/components/modal.mjs +3 -0
  52. package/dist/components/nav.css +134 -32
  53. package/dist/components/nav.mjs +3 -0
  54. package/dist/components/pagination.css +105 -19
  55. package/dist/components/pagination.mjs +2 -0
  56. package/dist/components/panel.css +61 -6
  57. package/dist/components/panel.mjs +2 -0
  58. package/dist/components/pill.css +74 -14
  59. package/dist/components/pill.mjs +2 -0
  60. package/dist/components/popover.css +109 -7
  61. package/dist/components/popover.mjs +3 -0
  62. package/dist/components/progress.mjs +2 -0
  63. package/dist/components/rating.mjs +2 -0
  64. package/dist/components/result.mjs +2 -0
  65. package/dist/components/segmented.css +199 -50
  66. package/dist/components/segmented.mjs +2 -0
  67. package/dist/components/select.css +140 -39
  68. package/dist/components/select.mjs +3 -0
  69. package/dist/components/sidebar.css +162 -34
  70. package/dist/components/sidebar.mjs +3 -0
  71. package/dist/components/skeleton.mjs +2 -0
  72. package/dist/components/slider.css +169 -31
  73. package/dist/components/slider.mjs +3 -0
  74. package/dist/components/spinner.mjs +2 -0
  75. package/dist/components/stat.css +103 -8
  76. package/dist/components/stat.mjs +2 -0
  77. package/dist/components/steps.css +105 -31
  78. package/dist/components/steps.mjs +2 -0
  79. package/dist/components/switch.css +171 -54
  80. package/dist/components/switch.mjs +2 -0
  81. package/dist/components/table.css +114 -12
  82. package/dist/components/table.mjs +2 -0
  83. package/dist/components/tabs.css +104 -17
  84. package/dist/components/tabs.mjs +3 -0
  85. package/dist/components/tag-input.css +119 -23
  86. package/dist/components/tag-input.mjs +3 -0
  87. package/dist/components/timeline.mjs +2 -0
  88. package/dist/components/toast.css +85 -12
  89. package/dist/components/toast.mjs +3 -0
  90. package/dist/components/tooltip.css +86 -26
  91. package/dist/components/tooltip.mjs +2 -0
  92. package/dist/components/tree.mjs +3 -0
  93. package/dist/components/upload.css +144 -17
  94. package/dist/components/upload.mjs +3 -0
  95. package/dist/index.mjs +629 -48
  96. package/dist/registry.json +197 -2
  97. package/dist/retro-effects.css +264 -0
  98. package/dist/tokens/colors.css +18 -90
  99. package/dist/tokens/retro-effects.css +264 -0
  100. package/dist/tokens/style-profiles.css +47 -47
  101. package/dist/tokens.css +65 -137
  102. package/dist/ui.css +4861 -1019
  103. package/package.json +9 -1
package/dist/cli.mjs ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * @icen.ai/ui — CLI:按需引入脚手架
4
+ * 不做 shadcn 式源码拷贝(本包是单一事实源,消费侧一律引包内产物),
5
+ * 只解析 slug → 打印正确的一行引入(kit 入口已自动带上 CSS 与 behavior)。
6
+ *
7
+ * 用法:
8
+ * bunx --bun @icen.ai/ui add <slug...> 打印引入行(多个 slug 空格分隔)
9
+ * bunx --bun @icen.ai/ui list 列出全部组件 slug
10
+ * 数据源:同目录 registry.json(构建期由 scripts/slugs.mjs 生成)。
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ const registry = JSON.parse(readFileSync(new URL('./registry.json', import.meta.url), 'utf8'));
15
+ const SLUGS = registry.slugs ?? {};
16
+
17
+ const [cmd, ...rest] = process.argv.slice(2);
18
+ const slugs = rest.filter((a) => !a.startsWith('-'));
19
+
20
+ const HELP = `@icen.ai/ui — 按需引入脚手架
21
+
22
+ add <slug...> 打印组件的一行引入(CSS + behavior 自动带上)
23
+ list 列出全部 ${Object.keys(SLUGS).length} 个组件 slug
24
+
25
+ 示例:
26
+ bunx --bun @icen.ai/ui add breadcrumb
27
+ bunx --bun @icen.ai/ui add accordion tabs
28
+
29
+ 前提:已安装本包(bun add @icen.ai/ui),并在入口引入 tokens:
30
+ import '@icen.ai/ui/tokens.css';`;
31
+
32
+ function importLines(slug) {
33
+ const meta = SLUGS[slug];
34
+ const path = `@icen.ai/ui/kit/${slug}`;
35
+ if (meta.init) {
36
+ return [
37
+ `import { ${meta.init} } from '${path}';`,
38
+ `${meta.init}();`,
39
+ ];
40
+ }
41
+ const lines = [`import '${path}';`];
42
+ if (meta.exports) lines.push(`// 可用导出:${meta.exports.join(' / ')}`);
43
+ return lines;
44
+ }
45
+
46
+ function suggest(bad) {
47
+ const keys = Object.keys(SLUGS);
48
+ return keys.filter((k) => k.includes(bad) || bad.includes(k)).slice(0, 3);
49
+ }
50
+
51
+ if (cmd === 'add' && slugs.length > 0) {
52
+ let failed = false;
53
+ for (const slug of slugs) {
54
+ if (!SLUGS[slug]) {
55
+ const near = suggest(slug);
56
+ console.error(`✗ 未知组件「${slug}」${near.length ? `(是不是:${near.join(' / ')}?)` : '(用 list 查看全部 slug)'}`);
57
+ failed = true;
58
+ continue;
59
+ }
60
+ console.log(`\n ${slug}`);
61
+ for (const line of importLines(slug)) console.log(` ${line}`);
62
+ }
63
+ console.log('');
64
+ process.exit(failed ? 1 : 0);
65
+ } else if (cmd === 'list') {
66
+ for (const [slug, meta] of Object.entries(SLUGS)) {
67
+ const bits = [meta.css.replace(/\.css$/, '')];
68
+ if (meta.init) bits.push(meta.init);
69
+ console.log(` ${slug.padEnd(14)} ${bits.join(' · ')}`);
70
+ }
71
+ } else {
72
+ console.log(HELP);
73
+ process.exit(cmd === 'add' ? 1 : 0);
74
+ }
@@ -0,0 +1,3 @@
1
+ // @icen.ai/ui — kit: accordion(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './content.css';
3
+ export * from '../behaviors/accordion.mjs';
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: alert(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './feedback.css';
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: avatar(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './media.css';
@@ -1,31 +1,104 @@
1
1
  /*
2
2
  * @icen.ai/ui — Component: breadcrumb(面包屑)
3
- * 用法:<nav class="breadcrumb" aria-label="面包屑"><ol>
4
- * <li><a class="breadcrumb-link" href="…">首页</a><span class="breadcrumb-sep" aria-hidden="true"></span></li>
5
- * <li><span class="breadcrumb-current" aria-current="page">当前页</span></li>
6
- * </ol></nav>
7
- * 分隔符是 .breadcrumb-sep 的 ::after(›);超长折叠用 .breadcrumb-ellipsis。
3
+ *
4
+ * 完整契约:
5
+ * <nav class="breadcrumb" aria-label="面包屑">
6
+ * <ol>
7
+ * <li>
8
+ * <a class="breadcrumb-link" href="…"><svg>…</svg> 首页</a>
9
+ * <span class="breadcrumb-sep" aria-hidden="true"></span>
10
+ * </li>
11
+ * <li>
12
+ * <span class="breadcrumb-ellipsis" aria-hidden="true">…</span>
13
+ * <span class="breadcrumb-sep" aria-hidden="true"></span>
14
+ * </li>
15
+ * <li><span class="breadcrumb-current" aria-current="page">当前页</span></li>
16
+ * </ol>
17
+ * </nav>
18
+ *
19
+ * 分隔符是 .breadcrumb-sep::after(›);超长折叠用 .breadcrumb-ellipsis。
8
20
  */
9
21
 
10
- .breadcrumb { display: flex; align-items: center; font-size: 13px; }
11
- .breadcrumb ol { display: flex; align-items: center; gap: 6px; list-style: none; margin: 0; padding: 0; }
12
- .breadcrumb li { display: flex; align-items: center; gap: 6px; }
22
+ .breadcrumb {
23
+ display: flex;
24
+ align-items: center;
25
+ font-size: var(--density-font-size-md);
26
+ }
27
+ .breadcrumb ol {
28
+ display: flex;
29
+ align-items: center;
30
+ flex-wrap: wrap;
31
+ gap: var(--density-space-xs);
32
+ list-style: none;
33
+ margin: 0;
34
+ padding: 0;
35
+ }
36
+ .breadcrumb li {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: var(--density-space-xs);
40
+ min-width: 0;
41
+ }
13
42
 
14
43
  .breadcrumb-link {
15
- display: inline-flex; align-items: center; gap: 6px;
16
- color: var(--token-text-muted); text-decoration: none;
17
- transition: color var(--duration-fast) var(--ease-out);
44
+ display: inline-flex;
45
+ align-items: center;
46
+ gap: var(--density-space-xs);
47
+ color: var(--token-text-muted);
48
+ text-decoration: none;
49
+ max-width: 200px;
50
+ overflow: hidden;
51
+ text-overflow: ellipsis;
52
+ white-space: nowrap;
53
+ transition: color var(--icen-tx-fast);
54
+ }
55
+ .breadcrumb-link:hover {
56
+ color: var(--token-accent);
57
+ text-decoration: none;
58
+ }
59
+ .breadcrumb-link:focus-visible {
60
+ outline: none;
61
+ border-radius: var(--radius-xs);
62
+ box-shadow: var(--icen-ring);
63
+ }
64
+ .breadcrumb-link > svg {
65
+ width: 13px;
66
+ height: 13px;
67
+ flex-shrink: 0;
18
68
  }
19
- .breadcrumb-link:hover { color: var(--token-accent); text-decoration: none; }
20
- .breadcrumb-link > svg { width: 13px; height: 13px; flex-shrink: 0; }
21
69
 
22
- .breadcrumb-sep { color: var(--token-text-faint); font-size: 13px; line-height: 1; }
70
+ .breadcrumb-sep {
71
+ color: var(--token-text-faint);
72
+ font-size: var(--density-font-size-md);
73
+ line-height: 1;
74
+ user-select: none;
75
+ }
23
76
  .breadcrumb-sep::after { content: '›'; }
24
77
 
25
78
  .breadcrumb-current {
26
- display: inline-flex; align-items: center; gap: 6px;
27
- color: var(--token-text); font-weight: 500;
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: var(--density-space-xs);
82
+ color: var(--token-text);
83
+ font-weight: 500;
84
+ max-width: 240px;
85
+ overflow: hidden;
86
+ text-overflow: ellipsis;
87
+ white-space: nowrap;
88
+ }
89
+ .breadcrumb-current > svg {
90
+ width: 13px;
91
+ height: 13px;
92
+ flex-shrink: 0;
28
93
  }
29
- .breadcrumb-current > svg { width: 13px; height: 13px; flex-shrink: 0; }
30
94
 
31
- .breadcrumb-ellipsis { padding: 0 4px; color: var(--token-text-faint); }
95
+ .breadcrumb-ellipsis {
96
+ padding: 0 2px;
97
+ color: var(--token-text-faint);
98
+ user-select: none;
99
+ }
100
+
101
+ @media (max-width: 480px) {
102
+ .breadcrumb-link { max-width: 100px; }
103
+ .breadcrumb-current { max-width: 120px; }
104
+ }
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: breadcrumb(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './breadcrumb.css';
@@ -1,23 +1,183 @@
1
1
  /*
2
2
  * @icen.ai/ui — Component: btn(按钮)
3
- * 用法:<button class="btn btn-primary"> / .btn-danger / .btn-sm / .btn-block
3
+ *
4
+ * 完整契约:
5
+ * <button class="btn [.btn-primary|.btn-danger|.btn-ghost|.btn-link] [.btn-sm|.btn-lg] [.btn-block] [.btn-icon]
6
+ * [.is-loading] [.is-error] [disabled]|[aria-disabled=true]|[data-disabled]>
7
+ * [<svg class="btn-icon-start">…</svg>] 可选前置图标(与 .btn-icon 不同:.btn-icon 是纯图标钮)
8
+ * <span class="btn-text">文本</span>
9
+ * [<svg class="btn-icon-end">…</svg>] 可选后置图标
10
+ * </button>
11
+ *
12
+ * 变体:
13
+ * .btn 默认(surface 底 + outline-soft 描边)
14
+ * .btn-primary 实心 accent
15
+ * .btn-danger 透明底 + error 描边(hover 实心)
16
+ * .btn-ghost 全透明,hover 浮起 fill-2
17
+ * .btn-link link 文本按钮(无背景、underline on hover)
18
+ *
19
+ * 尺寸:默认 / .btn-sm / .btn-lg / .btn-icon(正方形)
20
+ * 状态:.is-loading 加 spinner + aria-busy;.is-error error 边;disabled/aria-disabled/data-disabled
4
21
  */
5
22
 
6
23
  .btn {
7
- display: inline-block; padding: 10px 22px;
8
- background: var(--token-surface); color: var(--token-text);
9
- border: 1px solid var(--token-line-soft); border-radius: var(--radius-sm);
10
- font-family: var(--font-mono); font-size: 13px; letter-spacing: 1px;
11
- cursor: pointer; transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
24
+ --btn-bg: var(--token-surface);
25
+ --btn-fg: var(--token-text);
26
+ --btn-border: var(--token-line-soft);
27
+ --btn-bg-hover: var(--token-surface-hover);
28
+ --btn-border-hover: var(--token-accent);
29
+
30
+ display: inline-flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ gap: 6px;
34
+ padding: 0 var(--density-space-md);
35
+ height: var(--density-input-height-md);
36
+ background: var(--btn-bg);
37
+ color: var(--btn-fg);
38
+ border: 1px solid var(--btn-border);
39
+ border-radius: var(--radius-md);
40
+ font-family: var(--font-body);
41
+ font-size: var(--density-font-size-md);
42
+ font-weight: 500;
43
+ letter-spacing: 0.01em;
44
+ white-space: nowrap;
45
+ cursor: pointer;
46
+ user-select: none;
47
+ -webkit-tap-highlight-color: transparent;
48
+ text-decoration: none;
49
+ transition:
50
+ background-color var(--icen-tx-fast),
51
+ border-color var(--icen-tx-fast),
52
+ color var(--icen-tx-fast),
53
+ box-shadow var(--icen-tx-fast),
54
+ transform var(--icen-tx-fast);
55
+ }
56
+ .btn:hover {
57
+ background: var(--btn-bg-hover);
58
+ border-color: var(--btn-border-hover);
59
+ }
60
+ .btn:active { transform: var(--icen-press-soft); }
61
+ .btn:focus-visible {
62
+ outline: none;
63
+ box-shadow: var(--icen-ring);
64
+ }
65
+ .btn:disabled,
66
+ .btn[aria-disabled='true'],
67
+ .btn[data-disabled],
68
+ .btn.is-loading {
69
+ cursor: not-allowed;
70
+ opacity: var(--icen-disabled-opacity);
71
+ transform: none;
72
+ pointer-events: none;
73
+ }
74
+
75
+ /* ── 变体 ── */
76
+ .btn-primary {
77
+ --btn-bg: var(--token-accent);
78
+ --btn-fg: var(--token-on-accent, #fff);
79
+ --btn-border: var(--token-accent);
80
+ --btn-bg-hover: var(--token-accent-deep, color-mix(in srgb, var(--token-accent) 85%, black));
81
+ --btn-border-hover: var(--token-accent-deep, var(--token-accent));
82
+ }
83
+ .btn-primary:hover {
84
+ box-shadow: var(--token-shadow-elevated);
85
+ }
86
+
87
+ .btn-danger {
88
+ --btn-fg: var(--token-error);
89
+ --btn-border: color-mix(in srgb, var(--token-error) 35%, transparent);
90
+ --btn-bg: transparent;
91
+ --btn-bg-hover: var(--token-error);
92
+ --btn-border-hover: var(--token-error);
93
+ }
94
+ .btn-danger:hover { color: var(--token-on-accent, #fff); }
95
+
96
+ .btn-ghost {
97
+ --btn-bg: transparent;
98
+ --btn-border: transparent;
99
+ --btn-bg-hover: var(--token-fill-2);
100
+ --btn-border-hover: transparent;
101
+ }
102
+
103
+ .btn-link {
104
+ --btn-bg: transparent;
105
+ --btn-border: transparent;
106
+ --btn-fg: var(--token-accent);
107
+ --btn-bg-hover: transparent;
108
+ --btn-border-hover: transparent;
109
+ height: auto;
110
+ padding: 0;
111
+ border: 0;
112
+ border-radius: 0;
113
+ text-decoration: none;
114
+ }
115
+ .btn-link:hover {
116
+ text-decoration: underline;
117
+ text-underline-offset: 3px;
118
+ background: transparent;
12
119
  }
13
- .btn:hover { border-color: var(--token-accent); }
14
- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
15
120
 
16
- .btn-primary { background: var(--token-accent); border-color: var(--token-accent); color: var(--token-on-accent, #fff); }
17
- .btn-primary:hover { background: var(--token-accent-deep); border-color: var(--token-accent-deep); }
121
+ /* ── 尺寸 ── */
122
+ .btn-sm {
123
+ height: var(--density-input-height-sm);
124
+ padding: 0 var(--density-space-sm);
125
+ font-size: var(--density-font-size-sm);
126
+ gap: 4px;
127
+ }
128
+ .btn-lg {
129
+ height: var(--density-input-height-lg);
130
+ padding: 0 var(--density-space-lg);
131
+ font-size: var(--density-font-size-lg);
132
+ gap: 8px;
133
+ }
134
+ .btn-icon {
135
+ width: var(--density-input-height-md);
136
+ padding: 0;
137
+ gap: 0;
138
+ }
139
+ .btn-icon.btn-sm { width: var(--density-input-height-sm); }
140
+ .btn-icon.btn-lg { width: var(--density-input-height-lg); }
18
141
 
19
- .btn-danger { color: var(--token-error); border-color: color-mix(in srgb, var(--token-error) 35%, transparent); }
20
- .btn-danger:hover { border-color: var(--token-error); }
142
+ .btn-block {
143
+ display: flex;
144
+ width: 100%;
145
+ }
146
+
147
+ /* ── 状态 ── */
148
+ .btn.is-error {
149
+ --btn-border: var(--token-error);
150
+ --btn-border-hover: var(--token-error);
151
+ }
21
152
 
22
- .btn-block { display: block; width: 100%; text-align: center; }
23
- .btn-sm { padding: 6px 14px; font-size: 12px; }
153
+ /* 加载态:左侧 spinner 占位 + 文本保留(避免重排)。
154
+ behavior 在挂 .is-loading 时把 [aria-busy=true] 也挂上。 */
155
+ .btn.is-loading { position: relative; color: transparent; }
156
+ .btn.is-loading::after {
157
+ content: '';
158
+ position: absolute;
159
+ top: 50%; left: 50%;
160
+ width: 14px; height: 14px;
161
+ margin: -7px 0 0 -7px;
162
+ border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
163
+ border-top-color: currentColor;
164
+ border-radius: 50%;
165
+ animation: btn-spin var(--duration-slow) linear infinite;
166
+ opacity: 1; /* 覆盖 .pressable::after 渐变(若与 .pressable 同用) */
167
+ background: none;
168
+ pointer-events: none;
169
+ }
170
+ @keyframes btn-spin {
171
+ to { transform: rotate(360deg); }
172
+ }
173
+
174
+ /* 图标按钮内部 svg 默认尺寸对齐字号 */
175
+ .btn-icon-start, .btn-icon-end {
176
+ width: 1.1em;
177
+ height: 1.1em;
178
+ flex-shrink: 0;
179
+ }
180
+
181
+ @media (prefers-reduced-motion: reduce) {
182
+ .btn.is-loading::after { animation-duration: 1.2s; }
183
+ }
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: btn(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './btn.css';
@@ -1,7 +1,26 @@
1
1
  /*
2
2
  * @icen.ai/ui — Component: card(组合卡片容器)
3
- * 包含:.card 四变体(默认 / elevated / outlined / glass)+ .card--interactive + .is-selected
4
- * 子结构:.card-head / .card-title / .card-desc / .card-body / .card-foot / .card-meta / .card-action
3
+ *
4
+ * 完整契约:
5
+ * <div class="card [.card--elevated|.card--outlined|.card--glass|.card--flat|.card--interactive] [.is-selected]">
6
+ * <div class="card-media"> ← 可选:封面图区
7
+ * <img src="…" />
8
+ * <span class="card-tag">置顶</span> ← 可选:图上徽标
9
+ * </div>
10
+ * <div class="card-head">
11
+ * <div>
12
+ * <h3 class="card-title">标题</h3>
13
+ * <p class="card-desc">副标题(可选)</p>
14
+ * </div>
15
+ * <div class="card-action"><button>…</button></div>
16
+ * </div>
17
+ * <div class="card-body">…</div>
18
+ * <div class="card-foot">
19
+ * <div class="card-meta">…</div>
20
+ * <div class="card-action">…</div>
21
+ * </div>
22
+ * </div>
23
+ *
5
24
  * 规格源自 opengal src/presentation/ui/data/card.tsx。
6
25
  */
7
26
 
@@ -10,11 +29,22 @@
10
29
  border: 1px solid var(--token-outline-soft);
11
30
  box-shadow: var(--token-shadow-inset);
12
31
  border-radius: var(--radius-lg);
32
+ overflow: hidden;
13
33
  }
14
34
 
15
35
  /* ── 变体 ── */
16
- .card--elevated { box-shadow: var(--token-shadow-card); }
17
- .card--outlined { border-color: var(--token-outline); box-shadow: none; }
36
+ .card--elevated {
37
+ box-shadow: var(--token-shadow-card);
38
+ border-color: transparent;
39
+ }
40
+ .card--outlined {
41
+ border-color: var(--token-outline);
42
+ box-shadow: none;
43
+ }
44
+ .card--flat {
45
+ box-shadow: none;
46
+ background: var(--token-bg-soft);
47
+ }
18
48
  .card--glass {
19
49
  background: var(--token-surface-glass);
20
50
  backdrop-filter: blur(12px);
@@ -23,34 +53,104 @@
23
53
  }
24
54
 
25
55
  /* ── 修饰 ── */
26
- /* 可交互:cursor + .lift 基元同款效果(纯 CSS 无法组合类,规则在此复制一份) */
27
56
  .card--interactive {
28
57
  cursor: pointer;
29
- transition: border-color var(--duration-slow) var(--ease-out),
30
- box-shadow var(--duration-slow) var(--ease-out),
31
- transform var(--duration-slow) var(--ease-out);
58
+ transition:
59
+ border-color var(--icen-tx-slow),
60
+ box-shadow var(--icen-tx-slow),
61
+ transform var(--icen-tx-slow);
32
62
  }
33
63
  .card--interactive:hover {
34
64
  transform: translateY(-1px);
35
65
  border-color: color-mix(in srgb, var(--token-accent) 26%, var(--token-outline-soft));
36
66
  box-shadow: var(--token-shadow-card);
37
67
  }
68
+ .card--interactive:focus-within {
69
+ border-color: var(--token-accent);
70
+ }
38
71
  .card.is-selected {
39
- box-shadow: 0 0 0 2px var(--token-accent), 0 0 0 4px var(--token-bg);
72
+ box-shadow: var(--icen-ring);
73
+ }
74
+
75
+ /* ── 媒体封面 ── */
76
+ .card-media {
77
+ position: relative;
78
+ width: 100%;
79
+ aspect-ratio: 16 / 9;
80
+ overflow: hidden;
81
+ background: var(--token-bg-soft);
82
+ }
83
+ .card-media > img,
84
+ .card-media > video {
85
+ width: 100%;
86
+ height: 100%;
87
+ object-fit: cover;
88
+ }
89
+ .card-tag {
90
+ position: absolute;
91
+ top: var(--density-space-xs);
92
+ left: var(--density-space-xs);
93
+ padding: 2px var(--density-space-xs);
94
+ font-size: var(--density-font-size-xs);
95
+ font-weight: 600;
96
+ color: var(--token-on-accent, #fff);
97
+ background: color-mix(in srgb, var(--token-text) 70%, transparent);
98
+ border-radius: var(--radius-xs);
99
+ backdrop-filter: blur(8px);
100
+ -webkit-backdrop-filter: blur(8px);
40
101
  }
41
102
 
42
103
  /* ── 子结构 ── */
43
104
  .card-head {
44
- display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
45
- padding: 16px;
105
+ display: flex;
106
+ align-items: flex-start;
107
+ justify-content: space-between;
108
+ gap: var(--density-space-sm);
109
+ padding: var(--density-space-md);
110
+ }
111
+ .card-title {
112
+ font-size: var(--density-font-size-lg);
113
+ font-weight: 600;
114
+ color: var(--token-text);
115
+ line-height: 1.3;
116
+ margin: 0;
117
+ font-family: var(--font-heading);
118
+ }
119
+ .card-desc {
120
+ font-size: var(--density-font-size-sm);
121
+ color: var(--token-text-muted);
122
+ margin: 2px 0 0;
123
+ line-height: 1.4;
46
124
  }
47
- .card-title { font-size: 15px; font-weight: 600; color: var(--token-text); line-height: 1.25; margin: 0; }
48
- .card-desc { font-size: 12px; color: var(--token-text-muted); margin: 2px 0 0; }
49
- .card-body { padding: 0 16px 16px; font-size: 13px; color: var(--token-text-muted); }
125
+ .card-body {
126
+ padding: 0 var(--density-space-md) var(--density-space-md);
127
+ font-size: var(--density-font-size-md);
128
+ color: var(--token-text-muted);
129
+ line-height: 1.6;
130
+ }
131
+ .card-body > p:first-child { margin-top: 0; }
132
+ .card-body > p:last-child { margin-bottom: 0; }
133
+
50
134
  .card-foot {
51
- display: flex; align-items: center; gap: 8px;
52
- padding: 12px 16px;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: var(--density-space-sm);
138
+ padding: var(--density-space-sm) var(--density-space-md);
53
139
  border-top: 1px solid var(--token-outline-soft);
140
+ background: color-mix(in srgb, var(--token-bg-soft) 50%, transparent);
141
+ }
142
+ .card-meta {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: var(--density-space-xs);
146
+ font-size: var(--density-font-size-xs);
147
+ color: var(--token-text-faint);
148
+ }
149
+ .card-action {
150
+ margin-left: auto;
151
+ flex-shrink: 0;
152
+ }
153
+
154
+ @media (prefers-reduced-motion: reduce) {
155
+ .card--interactive { transition: none; }
54
156
  }
55
- .card-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--token-text-faint); }
56
- .card-action { margin-left: auto; }
@@ -0,0 +1,2 @@
1
+ // @icen.ai/ui — kit: card(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './card.css';
@@ -0,0 +1,3 @@
1
+ // @icen.ai/ui — kit: carousel(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './media.css';
3
+ export * from '../behaviors/carousel.mjs';