@icen.ai/ui 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +29 -16
  2. package/dist/base.css +389 -48
  3. package/dist/behaviors/accordion.mjs +20 -0
  4. package/dist/behaviors/carousel.mjs +30 -1
  5. package/dist/behaviors/charts.mjs +168 -0
  6. package/dist/behaviors/copy.mjs +4 -3
  7. package/dist/behaviors/datatable.mjs +748 -0
  8. package/dist/behaviors/input.mjs +20 -1
  9. package/dist/behaviors/modal.mjs +14 -0
  10. package/dist/behaviors/nav.mjs +31 -4
  11. package/dist/behaviors/sidebar.mjs +12 -0
  12. package/dist/behaviors/slider.mjs +35 -3
  13. package/dist/behaviors/tabs.mjs +100 -9
  14. package/dist/behaviors/tag-input.mjs +11 -0
  15. package/dist/behaviors/theme.mjs +26 -0
  16. package/dist/behaviors/toast.mjs +83 -15
  17. package/dist/behaviors/tree.mjs +57 -1
  18. package/dist/behaviors/upload.mjs +2 -3
  19. package/dist/cli.mjs +74 -0
  20. package/dist/components/accordion.mjs +3 -0
  21. package/dist/components/alert.mjs +2 -0
  22. package/dist/components/avatar.mjs +2 -0
  23. package/dist/components/breadcrumb.css +91 -18
  24. package/dist/components/breadcrumb.mjs +2 -0
  25. package/dist/components/btn.css +174 -14
  26. package/dist/components/btn.mjs +2 -0
  27. package/dist/components/card.css +118 -18
  28. package/dist/components/card.mjs +2 -0
  29. package/dist/components/carousel.mjs +3 -0
  30. package/dist/components/charts.css +225 -33
  31. package/dist/components/charts.mjs +3 -0
  32. package/dist/components/content.css +256 -74
  33. package/dist/components/context-menu.mjs +3 -0
  34. package/dist/components/datatable.css +343 -0
  35. package/dist/components/datatable.mjs +4 -0
  36. package/dist/components/desc.mjs +2 -0
  37. package/dist/components/dropdown.mjs +3 -0
  38. package/dist/components/empty.css +50 -10
  39. package/dist/components/empty.mjs +2 -0
  40. package/dist/components/feedback.css +158 -47
  41. package/dist/components/form.css +139 -22
  42. package/dist/components/input.css +165 -45
  43. package/dist/components/input.mjs +3 -0
  44. package/dist/components/kbd.mjs +2 -0
  45. package/dist/components/layout.mjs +2 -0
  46. package/dist/components/list.mjs +2 -0
  47. package/dist/components/media-card.mjs +2 -0
  48. package/dist/components/media.css +187 -40
  49. package/dist/components/menu.css +159 -32
  50. package/dist/components/modal.css +140 -34
  51. package/dist/components/modal.mjs +3 -0
  52. package/dist/components/nav.css +134 -32
  53. package/dist/components/nav.mjs +3 -0
  54. package/dist/components/pagination.css +105 -19
  55. package/dist/components/pagination.mjs +2 -0
  56. package/dist/components/panel.css +61 -6
  57. package/dist/components/panel.mjs +2 -0
  58. package/dist/components/pill.css +74 -14
  59. package/dist/components/pill.mjs +2 -0
  60. package/dist/components/popover.css +109 -7
  61. package/dist/components/popover.mjs +3 -0
  62. package/dist/components/progress.mjs +2 -0
  63. package/dist/components/rating.mjs +2 -0
  64. package/dist/components/result.mjs +2 -0
  65. package/dist/components/segmented.css +199 -50
  66. package/dist/components/segmented.mjs +2 -0
  67. package/dist/components/select.css +140 -39
  68. package/dist/components/select.mjs +3 -0
  69. package/dist/components/sidebar.css +162 -34
  70. package/dist/components/sidebar.mjs +3 -0
  71. package/dist/components/skeleton.mjs +2 -0
  72. package/dist/components/slider.css +169 -31
  73. package/dist/components/slider.mjs +3 -0
  74. package/dist/components/spinner.mjs +2 -0
  75. package/dist/components/stat.css +103 -8
  76. package/dist/components/stat.mjs +2 -0
  77. package/dist/components/steps.css +105 -31
  78. package/dist/components/steps.mjs +2 -0
  79. package/dist/components/switch.css +171 -54
  80. package/dist/components/switch.mjs +2 -0
  81. package/dist/components/table.css +114 -12
  82. package/dist/components/table.mjs +2 -0
  83. package/dist/components/tabs.css +104 -17
  84. package/dist/components/tabs.mjs +3 -0
  85. package/dist/components/tag-input.css +119 -23
  86. package/dist/components/tag-input.mjs +3 -0
  87. package/dist/components/timeline.mjs +2 -0
  88. package/dist/components/toast.css +85 -12
  89. package/dist/components/toast.mjs +3 -0
  90. package/dist/components/tooltip.css +86 -26
  91. package/dist/components/tooltip.mjs +2 -0
  92. package/dist/components/tree.mjs +3 -0
  93. package/dist/components/upload.css +144 -17
  94. package/dist/components/upload.mjs +3 -0
  95. package/dist/index.mjs +623 -42
  96. package/dist/registry.json +197 -2
  97. package/dist/retro-effects.css +264 -0
  98. package/dist/tokens/colors.css +18 -90
  99. package/dist/tokens/retro-effects.css +264 -0
  100. package/dist/tokens/style-profiles.css +47 -47
  101. package/dist/tokens.css +65 -137
  102. package/dist/ui.css +4861 -1019
  103. package/package.json +9 -1
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # @icen.ai/ui — Icen Design System
2
2
 
3
- icen.ai 全生态的统一设计系统:**设计 tokens(6 色彩预设 × 明暗双模 × 风格配置)+ 无框架组件 CSS + 行为 JS**。工程形态与 `@icen.ai/cli` 一致(Bun + TS + tsup + ESM + MIT)。
3
+ icen.ai 全生态的统一设计系统:**设计 tokens(6 色彩预设 × 明暗双模 × 3 风格 profile)+ 无框架组件 CSS(31 组件)+ 行为 JS(19 behaviors)**。工程形态与 `@icen.ai/cli` 一致(Bun + TS + tsup + ESM + MIT)。
4
4
 
5
5
  - 色彩预设:`clay`(默认,陶土橙 × 纸白 = icen 品牌)/ `piano` / `art` / `vangogh` / `ink` / `retro`,各含 `.dark` 变体
6
- - 风格配置:`.style-modern` / `.style-retro` / `.style-terminal`(几何/密度/动效/字体 token,与色彩正交)
7
- - 组件:btn / panel / pill / form / tabs / stat / table / toast(全部消费 `--token-*`,框架无关)
8
- - 行为:`theme`(预设/明暗/风格切换 + localStorage + 防闪烁语义)、`tabs`、`toast`、`copy`
6
+ - 风格配置:`.style-modern` / `.style-retro` / `.style-terminal`(几何 / 密度 / 动效 / 字体 token,与色彩正交)
7
+ - 可选效果层:`retro-effects.css`(CRT 扫描线 / 颗粒 / 像素边框 / 打字机光标 —— 仅 `.style-retro` 激活时生效,按需 import)
8
+ - 组件:btn / form / input / select / slider / switch / tag-input / upload / modal / popover / tooltip / menu(含 dropdown + context-menu)/ tabs / nav / sidebar / breadcrumb / pagination / steps / segmented / accordion / tree / carousel / charts / table / stat / card / panel / pill / empty / feedback / content / media(31 个)
9
+ - 行为:theme / tabs / toast / copy / input / select / slider / tag-input / upload / modal / dropdown / popover / context-menu / accordion / tree / carousel / charts / nav / sidebar(19 个,全部 SSR 守卫 + 幂等 init + textContent-only)
9
10
 
10
11
  ## 使用
11
12
 
@@ -16,6 +17,8 @@ bun add @icen.ai/ui
16
17
  ```css
17
18
  @import '@icen.ai/ui/tokens.css';
18
19
  @import '@icen.ai/ui/ui.css'; /* tokens + base + 全部组件;也可按 ./components/btn.css 单引 */
20
+ /* 可选:仅 .style-retro 下生效的像素效果层 */
21
+ @import '@icen.ai/ui/retro-effects.css';
19
22
  ```
20
23
 
21
24
  ```html
@@ -23,11 +26,18 @@ bun add @icen.ai/ui
23
26
  ```
24
27
 
25
28
  ```ts
26
- import { initTheme, toggleDark, setTheme, initTabs, initCopy, toast } from '@icen.ai/ui';
27
- initTheme(); // 尽早调用(或内联同步脚本防闪烁,见 accounts Base.astro)
28
- initTabs(); // [data-tabs] > [data-tab] + [data-tab-panel] 契约
29
- initCopy(); // .copy-btn[data-copy] 委托
30
- toast.ok('已保存');
29
+ import {
30
+ initTheme, toggleDark, setTheme,
31
+ initTabs, initCopy, initInput, initSelect, initSlider, initTagInput,
32
+ initUpload, initModal, initDropdown, initPopover, initContextMenu,
33
+ initAccordion, initTree, initCarousel, initCharts, initNav, initSidebar,
34
+ toast,
35
+ } from '@icen.ai/ui';
36
+
37
+ initTheme(); // 尽早调用(或内联同步脚本防闪烁,见 accounts Base.astro)
38
+ initTabs(); // [data-tabs] > [data-tab] + [data-tab-panel] 契约
39
+ initCopy(); // .copy-btn[data-copy] 委托
40
+ toast.ok('已保存'); // 或 toast.action('已删除', '撤销', undo)
31
41
  ```
32
42
 
33
43
  ## 文档站(已上线 <https://ui.icen.ai>)
@@ -48,18 +58,21 @@ bun install
48
58
  bun run build # tsup(behaviors → dist/*.mjs)+ scripts/build-css.ts(css → dist/)
49
59
  ```
50
60
 
51
- 产物:`dist/tokens.css`(三 token 文件拼合)、`dist/ui.css`(tokens+base+组件)、`dist/base.css`、`dist/tokens/*`、`dist/components/*`、`dist/behaviors/*.mjs`、`dist/index.mjs`、`dist/registry.json`。
61
+ 产物:`dist/tokens.css`(colors + style-profiles + typography 拼合)、`dist/ui.css`(tokens+base+组件)、`dist/base.css`、`dist/retro-effects.css`(按需)、`dist/tokens/*`、`dist/components/*`、`dist/behaviors/*.mjs`、`dist/index.mjs`、`dist/registry.json`(含 tokens/tokensExtras/base/components/behaviors 清单)。
52
62
 
53
63
  ## 目录
54
64
 
55
65
  ```
56
- src/tokens/colors.css # 6 预设 × 明暗(移植自 opengal 的 Icen Design System;
57
- # clay 基值已原地校准到 accounts.icen.ai 视觉)
58
- src/tokens/style-profiles.css # density/motion/typography + .style-*
66
+ src/tokens/colors.css # 6 预设 × 明暗(clay 基值已校准到 accounts.icen.ai 视觉)
67
+ src/tokens/style-profiles.css # density/motion/typography + .style-modern/.retro/.terminal
68
+ # retro 段选择器引用 base.css 的 .pressable/.control/.lift
59
69
  src/tokens/typography.css # icen canonical 字体栈(IBM Plex Mono + CJK 宋体)
60
- src/base.css # 元素基线 + 无箭头细滚动条(全部消费 token)
61
- src/components/*.css # 8 个无框架组件
62
- src/behaviors/*.ts # theme / tabs / toast / copy
70
+ src/tokens/retro-effects.css # 可选效果层:CRT/扫描线/像素工具类(不进 tokens.css 拼合)
71
+ src/base.css # 元素基线 + 滚动条 + z-index 标尺 + 6 个交互基元
72
+ # .pressable / .control / .field / .lift / .surface-elevated / .focus-ring
73
+ # + a11y 工具类 .sr-only / .skip-link + 工具类 .mono/.dim/.faint/.numeric
74
+ src/components/*.css # 31 个无框架组件
75
+ src/behaviors/*.ts # 19 个 behaviors
63
76
  scripts/build-css.ts # CSS 产物构建(拼合 + registry.json)
64
77
  site/ # 文档站骨架(ui.icen.ai,独立 Astro 应用)
65
78
  ```
package/dist/base.css CHANGED
@@ -1,22 +1,137 @@
1
1
  /*
2
- * @icen.ai/ui — Base(元素基线 + 滚动条)
3
- * 全部消费 --token-*;任何 preset/明暗/style profile 下自洽。
2
+ * @icen.ai/ui — Base(元素基线 + 交互基元 + 统一抽象层 + 滚动条)
3
+ *
4
+ * 全部消费 --token-*;任何 preset / 明暗 / style profile 下自洽。
4
5
  * 在 tokens 之后、components 之前引入。
6
+ *
7
+ * ════════════════════════════════════════════════════════════════════════
8
+ * icen 组件契约(统一抽象)
9
+ * ════════════════════════════════════════════════════════════════════════
10
+ * 所有 31 个组件遵循同一套抽象,由两层共同支撑:
11
+ *
12
+ * ── 层 1:--icen-* 原语 token(所有组件自洽消费)
13
+ * --icen-ring / --icen-ring-soft / --icen-ring-error ← 聚焦环
14
+ * --icen-hover-border ← 悬停边框过渡
15
+ * --icen-disabled-opacity ← 禁用透明度
16
+ * --icen-press ← 按下形变
17
+ * --icen-control-focus-shadow / -border ← 表单控件聚焦
18
+ * --icen-tx-fast / -normal / -slow ← 统一过渡时长+缓动
19
+ *
20
+ * 组件直接写:
21
+ * .btn:focus-visible { box-shadow: var(--icen-ring); }
22
+ * .btn:disabled { opacity: var(--icen-disabled-opacity); }
23
+ *
24
+ * 风格 profile 覆盖原语:
25
+ * .style-retro { --icen-ring: 2px solid var(--token-accent); --icen-press: translate(2px,2px); }
26
+ *
27
+ * ── 层 2:6 个交互基元 class(HTML 显式组合 API)
28
+ * .pressable / .control / .field / .lift / .surface-elevated / .focus-ring
29
+ *
30
+ * 用法(任选其一):
31
+ * <button class="btn pressable focus-ring"> ← 类组合,体积最小
32
+ * <button class="btn"> ← 自洽,CSS 引用同一原语 token
33
+ *
34
+ * ── 命名约定(所有组件统一)
35
+ * 根类: .component
36
+ * 变体: .component--variant / .component-primary(语义变体可省 --)
37
+ * 尺寸: .component--sm / --lg / --xl
38
+ * 状态: .is-loading / .is-error / .is-disabled / .is-open / .is-selected /
39
+ * .is-focused / .is-active / .is-empty / .is-filled
40
+ * aria 等价: [aria-busy=true]↔.is-loading、[data-invalid=true]↔.is-error、
41
+ * [aria-disabled=true]/[data-disabled]↔.is-disabled、
42
+ * [aria-expanded=true]↔.is-open、[aria-selected=true]/[aria-current=page]↔.is-active
43
+ *
44
+ * ── Token 纪律
45
+ * 颜色: 仅 --token-success / -warning / -error / -info / -accent(语义)
46
+ * 间距: --density-space-*
47
+ * 字号: --density-font-size-*
48
+ * 控件高度: --density-input-height-*
49
+ * 圆角: --radius-*
50
+ * 阴影: --token-shadow-*
51
+ * 时长/缓动: --duration-* / --ease-*(或直接用 --icen-tx-*)
52
+ * z-index: --z-*
53
+ *
54
+ * 暴露的契约:
55
+ * - 元素基线:box-sizing / body / a / code / pre / img / ::selection
56
+ * - 滚动条:贴 --token-scrollbar-*(retro profile 另有硬覆盖)
57
+ * - 工具类:.mono / .dim / .faint / .sr-only / .skip-link
58
+ * - z-index 标尺:--z-* (浮层一律 portal 到 body)
59
+ * - 交互基元 class:.pressable / .control / .field / .lift / .surface-elevated / .focus-ring / .motion
60
+ * - 状态约定:[aria-disabled] / [aria-busy] / [data-loading] / [data-disabled] / [data-invalid]
5
61
  */
6
62
 
63
+ /* ══════════════════════════════════════════════════════════════
64
+ icen 统一抽象层(--icen-* 原语 token)
65
+ ───────────────────────────────────────────────────────────────
66
+ 所有组件消费的"原语级"变量:聚焦环、悬停、按下、禁用、过渡。
67
+ 风格 profile(如 .style-retro)只需覆盖这里的少数变量,
68
+ 即可让 31 个组件整体换肤;组件自身无需感知风格切换。
69
+ ══════════════════════════════════════════════════════════════ */
70
+ :root {
71
+ /* ── 聚焦环(focus-visible) ── */
72
+ --icen-ring: 0 0 0 2px var(--token-bg), 0 0 0 4px color-mix(in srgb, var(--token-accent) 50%, transparent);
73
+ --icen-ring-soft: 0 0 0 3px color-mix(in srgb, var(--token-accent) 16%, transparent);
74
+ --icen-ring-error: 0 0 0 2px var(--token-bg), 0 0 0 4px color-mix(in srgb, var(--token-error) 50%, transparent);
75
+ --icen-ring-success: 0 0 0 2px var(--token-bg), 0 0 0 4px color-mix(in srgb, var(--token-success) 50%, transparent);
76
+
77
+ /* ── 悬停 / 按下 / 禁用 ── */
78
+ --icen-hover-border: color-mix(in srgb, var(--token-accent) 30%, var(--token-line));
79
+ --icen-hover-border-strong: color-mix(in srgb, var(--token-accent) 55%, var(--token-line));
80
+ --icen-press: translateY(1px) scale(0.985);
81
+ --icen-press-soft: translateY(1px);
82
+ --icen-disabled-opacity: 0.5;
83
+
84
+ /* ── 表单控件聚焦(wrapper 视角) ── */
85
+ --icen-control-focus-shadow: var(--token-shadow-inset), 0 0 0 3px color-mix(in srgb, var(--token-accent) 14%, transparent);
86
+ --icen-control-focus-border: color-mix(in srgb, var(--token-accent) 55%, var(--token-line));
87
+ --icen-control-error-shadow: var(--token-shadow-inset), 0 0 0 3px color-mix(in srgb, var(--token-error) 16%, transparent);
88
+
89
+ /* ── 统一过渡(时长 + 缓动打包) ── */
90
+ --icen-tx-fast: var(--duration-fast) var(--ease-out);
91
+ --icen-tx-normal: var(--duration-normal) var(--ease-out);
92
+ --icen-tx-slow: var(--duration-slow) var(--ease-out);
93
+ --icen-tx-bounce: var(--duration-normal) var(--ease-bounce);
94
+ }
95
+
96
+ /* ══════════════════════════════════════════════════════════════
97
+ 元素基线
98
+ ══════════════════════════════════════════════════════════════ */
99
+
7
100
  *, *::before, *::after { box-sizing: border-box; }
8
101
 
9
102
  html, body { margin: 0; padding: 0; }
10
103
 
104
+ html {
105
+ /* 触屏Pan-y 禁止水平捏合意外;具体页面可覆盖 */
106
+ -webkit-text-size-adjust: 100%;
107
+ text-size-adjust: 100%;
108
+ }
109
+
11
110
  body {
12
111
  background: var(--token-bg);
13
112
  color: var(--token-text);
113
+ font-family: "Numeric Math", var(--font-body);
14
114
  font-size: 14px;
115
+ line-height: var(--line-height-body, 1.6);
15
116
  min-height: 100vh;
117
+ -webkit-font-smoothing: antialiased;
118
+ -moz-osx-font-smoothing: grayscale;
119
+ text-rendering: optimizeLegibility;
120
+ font-feature-settings: "rlig" 1, "calt" 1;
121
+ font-variant-numeric: lining-nums tabular-nums;
16
122
  }
17
123
 
18
- a { color: var(--token-accent); text-decoration: none; }
19
- a:hover { text-decoration: underline; }
124
+ a {
125
+ color: var(--token-accent);
126
+ text-decoration: none;
127
+ transition: color var(--duration-fast) var(--ease-out);
128
+ }
129
+ a:hover { text-decoration: underline; text-underline-offset: 3px; }
130
+ a:focus-visible {
131
+ outline: 2px solid var(--token-accent);
132
+ outline-offset: 2px;
133
+ border-radius: var(--radius-xs);
134
+ }
20
135
 
21
136
  code, pre {
22
137
  font-family: var(--font-mono);
@@ -27,101 +142,327 @@ code { padding: 1px 5px; }
27
142
  pre { padding: 12px 14px; overflow-x: auto; }
28
143
  pre code { padding: 0; background: none; }
29
144
 
30
- button { font: inherit; }
31
- img { max-width: 100%; }
145
+ button { font: inherit; color: inherit; }
146
+ button:disabled { cursor: not-allowed; }
147
+ img, video, svg { max-width: 100%; height: auto; }
148
+ input, button, textarea, select { font: inherit; color: inherit; }
149
+
150
+ /* 标题用 heading 栈(typography.css 已声明,这里只兜底防缺字体场景) */
151
+ h1, h2, h3, h4, h5, h6 {
152
+ margin: 0;
153
+ line-height: var(--line-height-heading, 1.2);
154
+ }
32
155
 
156
+ /* 段落兜底(消费项目可以覆盖) */
157
+ p { margin: 0 0 1em; }
158
+
159
+ /* 选区:亮色 accent 25%,retro 在 retro-effects.css 里硬覆盖 */
33
160
  ::selection {
34
161
  background: color-mix(in srgb, var(--token-accent) 25%, transparent);
162
+ color: inherit;
163
+ }
164
+ .dark ::selection {
165
+ background: color-mix(in srgb, var(--token-accent) 35%, transparent);
35
166
  }
36
167
 
37
- /* ── 全局滚动条:细薄、无上下箭头、贴 token ── */
38
- ::-webkit-scrollbar { width: 8px; height: 8px; }
39
- ::-webkit-scrollbar-track { background: transparent; }
168
+ /* ══════════════════════════════════════════════════════════════
169
+ 全局滚动条:细薄、无上下箭头、贴 token
170
+ 尺寸/轨道/滑块/按钮全部走 --token-scrollbar-*,
171
+ 每个色彩预设可在自己的块里覆盖以表达品牌色差;
172
+ .style-retro 在 style-profiles.css 里另有像素化硬覆盖。
173
+ ══════════════════════════════════════════════════════════════ */
174
+
175
+ ::-webkit-scrollbar { width: var(--token-scrollbar-size); height: var(--token-scrollbar-size); }
176
+ ::-webkit-scrollbar-track { background: var(--token-scrollbar-track); }
40
177
  ::-webkit-scrollbar-thumb {
41
- background: color-mix(in srgb, var(--token-text) 16%, transparent);
178
+ background: var(--token-scrollbar-thumb);
42
179
  border-radius: 999px;
180
+ border: 2px solid transparent;
181
+ background-clip: padding-box;
43
182
  }
44
183
  ::-webkit-scrollbar-thumb:hover {
45
- background: color-mix(in srgb, var(--token-accent) 50%, transparent);
184
+ background: var(--token-scrollbar-thumb-hover);
185
+ background-clip: padding-box;
46
186
  }
47
187
  ::-webkit-scrollbar-button,
48
188
  ::-webkit-scrollbar-button:single-button { display: none; width: 0; height: 0; }
49
189
  ::-webkit-scrollbar-corner { background: transparent; }
50
- html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-text) 16%, transparent) transparent; }
190
+ html {
191
+ scrollbar-width: thin;
192
+ scrollbar-color: var(--token-scrollbar-thumb) var(--token-scrollbar-track);
193
+ }
194
+
195
+ /* ══════════════════════════════════════════════════════════════
196
+ 通用工具类
197
+ ══════════════════════════════════════════════════════════════ */
51
198
 
52
- /* ── 通用工具类 ── */
53
199
  .mono { font-family: var(--font-mono); }
54
200
  .dim { color: var(--token-text-muted); }
55
201
  .faint { color: var(--token-text-faint); }
202
+ .serif { font-family: var(--font-heading); }
203
+
204
+ /* 数字密集区:tabular-nums + 数学字体 */
205
+ .numeric {
206
+ font-family: "Numeric Math", var(--font-mono);
207
+ font-variant-numeric: lining-nums tabular-nums;
208
+ }
209
+
210
+ /* 无障碍:屏幕阅读器可见、视觉隐藏 */
211
+ .sr-only {
212
+ position: absolute;
213
+ width: 1px; height: 1px;
214
+ padding: 0; margin: -1px;
215
+ overflow: hidden;
216
+ clip: rect(0, 0, 0, 0);
217
+ clip-path: inset(50%);
218
+ white-space: nowrap;
219
+ border: 0;
220
+ }
221
+
222
+ /* 跳转链接:默认视觉隐藏,聚焦时显形(无障碍要求) */
223
+ .skip-link {
224
+ position: fixed;
225
+ top: var(--density-space-sm, 8px);
226
+ left: var(--density-space-sm, 8px);
227
+ z-index: var(--z-tooltip);
228
+ padding: 8px 14px;
229
+ background: var(--token-surface);
230
+ color: var(--token-text);
231
+ border: 1px solid var(--token-line);
232
+ border-radius: var(--radius-sm);
233
+ box-shadow: var(--token-shadow-elevated);
234
+ transform: translateY(-150%);
235
+ transition: transform var(--duration-normal) var(--ease-out);
236
+ }
237
+ .skip-link:focus {
238
+ transform: translateY(0);
239
+ outline: 2px solid var(--token-accent);
240
+ outline-offset: 2px;
241
+ }
56
242
 
57
- /* ── z-index 标尺(全组件统一引用,浮层一律 portal 到 body) ── */
243
+ /* ══════════════════════════════════════════════════════════════
244
+ z-index 标尺(全组件统一引用,浮层一律 portal 到 body)
245
+ 约定:
246
+ base → 文档流(默认)
247
+ raised → 卡片抬起、sticky 表头
248
+ sticky → 吸顶导航
249
+ chrome → app 外壳(sidebar/topbar)
250
+ toast → toast/通知(高于一切文档元素)
251
+ banner → 全屏横幅(紧急公告,高于 toast)
252
+ dialog → 模态对话框 + backdrop
253
+ popover → 弹出层/下拉菜单(高于 dialog 因为常常 portal 出去)
254
+ tooltip → 工具提示(永远在最上)
255
+ ══════════════════════════════════════════════════════════════ */
58
256
  :root {
59
257
  --z-base: 0;
60
258
  --z-raised: 10;
61
- --z-toast: 100;
259
+ --z-sticky: 50;
62
260
  --z-chrome: 200;
261
+ --z-toast: 400;
262
+ --z-banner: 450;
63
263
  --z-dialog: 500;
64
264
  --z-popover: 620;
65
265
  --z-tooltip: 700;
66
266
  }
67
267
 
68
- /* ── 交互基元(全组件地基,规格源自 opengal interaction-classes) ── */
268
+ /* ══════════════════════════════════════════════════════════════
269
+ 交互基元(全组件地基)
270
+ 规格:源 self-contained,命名空间不含 opengal 的 og-/vn- 前缀。
271
+ style-profiles.css 的 .style-retro 段会按 .pressable/.control/.lift
272
+ 这些类名施加像素化覆盖;改名前请同步修 style-profiles.css。
273
+ ══════════════════════════════════════════════════════════════ */
69
274
 
70
- /* 可按压:hover 顶部高光 + active 下陷缩放 */
275
+ /* ── .pressable — 可按压(按钮、卡片按钮、tag close 等)
276
+ hover:顶部高光 ::after 渐入;focus-visible:同样高光 + 描边
277
+ active:下陷 + 微缩;aria-busy:减淡;disabled / aria-disabled:50% + 禁手势 */
71
278
  .pressable {
72
- position: relative; overflow: hidden; will-change: transform;
73
- transition: background-color var(--duration-normal) var(--ease-out),
74
- border-color var(--duration-normal) var(--ease-out),
75
- box-shadow var(--duration-normal) var(--ease-out),
76
- color var(--duration-normal) var(--ease-out),
77
- opacity var(--duration-normal) var(--ease-out),
78
- transform var(--duration-normal) var(--ease-out);
279
+ position: relative;
280
+ overflow: hidden;
281
+ will-change: transform;
282
+ -webkit-tap-highlight-color: transparent;
283
+ transition: background-color var(--icen-tx-normal),
284
+ border-color var(--icen-tx-normal),
285
+ box-shadow var(--icen-tx-normal),
286
+ color var(--icen-tx-normal),
287
+ opacity var(--icen-tx-normal),
288
+ transform var(--icen-tx-normal);
79
289
  }
80
290
  .pressable::after {
81
- content: ''; position: absolute; inset: 0; border-radius: inherit;
82
- opacity: 0; pointer-events: none;
83
- background: linear-gradient(180deg, color-mix(in srgb, var(--token-surface) 24%, transparent), transparent 46%);
84
- transition: opacity var(--duration-normal) var(--ease-out);
85
- }
86
- .pressable:hover::after, .pressable:focus-visible::after { opacity: 1; }
87
- .pressable:active { transform: translateY(1px) scale(0.985); }
88
- .pressable:disabled, .pressable[aria-disabled='true'], .pressable[data-disabled] {
291
+ content: '';
292
+ position: absolute;
293
+ inset: 0;
294
+ border-radius: inherit;
295
+ opacity: 0;
296
+ pointer-events: none;
297
+ background: linear-gradient(180deg,
298
+ color-mix(in srgb, var(--token-surface) 24%, transparent),
299
+ transparent 46%);
300
+ transition: opacity var(--icen-tx-normal);
301
+ }
302
+ .pressable:hover::after,
303
+ .pressable:focus-visible::after { opacity: 1; }
304
+ .pressable:focus-visible {
305
+ outline: none;
306
+ box-shadow: var(--icen-ring);
307
+ }
308
+ .pressable:active {
309
+ transform: var(--icen-press);
310
+ }
311
+ .pressable[aria-busy='true'],
312
+ .pressable[data-loading] {
313
+ cursor: progress;
314
+ opacity: 0.7;
315
+ pointer-events: none;
316
+ }
317
+ .pressable:disabled,
318
+ .pressable[aria-disabled='true'],
319
+ .pressable[data-disabled] {
89
320
  transform: none;
321
+ opacity: var(--icen-disabled-opacity);
322
+ cursor: not-allowed;
323
+ pointer-events: none;
90
324
  }
91
- .pressable:disabled::after, .pressable[aria-disabled='true']::after, .pressable[data-disabled]::after { display: none; }
325
+ .pressable:disabled::after,
326
+ .pressable[aria-disabled='true']::after,
327
+ .pressable[data-disabled]::after { display: none; }
92
328
 
93
- /* 控件聚焦光晕(wrapper 式表单控件用 :focus-within) */
329
+ /* ── .control — 表单控件 wrapper 基元(包裹原生 input/textarea/select)
330
+ hover:边框稍深;focus-within:内嵌阴影 + accent 3px 光晕
331
+ :invalid / [data-invalid]:error 色边框 + error 色光晕 */
94
332
  .control {
95
- transition: background-color var(--duration-normal) var(--ease-out),
96
- border-color var(--duration-normal) var(--ease-out),
97
- box-shadow var(--duration-normal) var(--ease-out);
333
+ transition: background-color var(--icen-tx-normal),
334
+ border-color var(--icen-tx-normal),
335
+ box-shadow var(--icen-tx-normal);
336
+ }
337
+ .control:hover {
338
+ border-color: var(--icen-hover-border);
98
339
  }
99
340
  .control:focus-within {
100
- box-shadow: var(--token-shadow-inset), 0 0 0 3px color-mix(in srgb, var(--token-accent) 12%, transparent);
341
+ box-shadow: var(--icen-control-focus-shadow);
342
+ border-color: var(--icen-control-focus-border);
343
+ }
344
+ .control:where(:has(:invalid), [data-invalid='true']) {
345
+ border-color: var(--token-error);
346
+ }
347
+ .control:where(:has(:invalid), [data-invalid='true']):focus-within {
348
+ box-shadow: var(--icen-control-error-shadow);
349
+ border-color: var(--token-error);
350
+ }
351
+ /* 内部裸 input/textarea/select 不再有自己的边框/阴影,交给 wrapper 表达 */
352
+ .control > input:not([type='checkbox']):not([type='radio']):not([type='file']),
353
+ .control > textarea,
354
+ .control > select {
355
+ background: transparent;
356
+ border: 0;
357
+ outline: none;
358
+ box-shadow: none;
359
+ }
360
+
361
+ /* ── .field — 带 label / hint / error 的表单字段容器(取代旧 .vn-editable-field)
362
+ 契约:
363
+ <label class="field">
364
+ <span class="field-label">姓名</span>
365
+ <span class="control"><input /></span>
366
+ <span class="field-hint">必填</span>
367
+ </label> */
368
+ .field {
369
+ display: flex;
370
+ flex-direction: column;
371
+ gap: var(--density-space-xs);
372
+ }
373
+ .field-label {
374
+ font-size: var(--density-font-size-sm);
375
+ color: var(--token-text-muted);
376
+ letter-spacing: 0.02em;
101
377
  }
378
+ .field-hint {
379
+ font-size: var(--density-font-size-xs);
380
+ color: var(--token-text-faint);
381
+ }
382
+ .field-error {
383
+ font-size: var(--density-font-size-xs);
384
+ color: var(--token-error);
385
+ }
386
+ .field[data-invalid='true'] .field-label { color: var(--token-error); }
102
387
 
103
- /* 悬停浮起(可交互卡片) */
388
+ /* ── .lift — 可交互卡片悬停浮起(pressable 的轻量版,无 ::after 高光)
389
+ hover / focus-within:上移 1px + accent 边 + card 阴影 */
104
390
  .lift {
105
- transition: border-color var(--duration-slow) var(--ease-out),
106
- box-shadow var(--duration-slow) var(--ease-out),
107
- transform var(--duration-slow) var(--ease-out);
391
+ transition: border-color var(--icen-tx-slow),
392
+ box-shadow var(--icen-tx-slow),
393
+ transform var(--icen-tx-slow);
108
394
  }
109
- .lift:hover {
395
+ .lift:hover,
396
+ .lift:focus-within {
110
397
  transform: translateY(-1px);
111
398
  border-color: color-mix(in srgb, var(--token-accent) 26%, var(--token-outline-soft));
112
399
  box-shadow: var(--token-shadow-card);
113
400
  }
114
401
 
115
- /* 聚焦环(ring-2 + offset-1 等价) */
402
+ /* ── .surface-elevated — 提升的静态表面(非交互卡片)
403
+ 用于不需要 hover 反馈但需要脱离背景的面板/对话框内嵌区域 */
404
+ .surface-elevated {
405
+ background: var(--token-surface);
406
+ border: 1px solid var(--token-line-soft);
407
+ border-radius: var(--radius-md);
408
+ box-shadow: var(--token-shadow-card);
409
+ }
410
+
411
+ /* ── .focus-ring — 显式聚焦环工具类(ring-2 + offset-1 等价) */
116
412
  .focus-ring:focus-visible {
117
413
  outline: none;
118
- box-shadow: 0 0 0 1px var(--token-bg), 0 0 0 3px color-mix(in srgb, var(--token-accent) 60%, transparent);
414
+ box-shadow: var(--icen-ring);
119
415
  }
120
416
 
121
- /* 动效开关(reduced-motion 时全部关闭) */
417
+ /* ── .motion — 显式动画工具类(仅在 prefers-reduced-motion: no-preference 时生效) */
122
418
  @media (prefers-reduced-motion: no-preference) {
123
- .motion { transition: all var(--duration-normal) var(--ease-out); }
419
+ .motion { transition: all var(--icen-tx-normal); }
124
420
  }
421
+
422
+ /* ══════════════════════════════════════════════════════════════
423
+ 动效与无障碍兜底
424
+ ══════════════════════════════════════════════════════════════ */
425
+
426
+ /* reduced-motion:所有动画/过渡压缩到 ~0ms(保留可访问性最小可视信号) */
125
427
  @media (prefers-reduced-motion: reduce) {
126
- *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
428
+ *, *::before, *::after {
429
+ animation-duration: 0.01ms !important;
430
+ animation-iteration-count: 1 !important;
431
+ transition-duration: 0.01ms !important;
432
+ scroll-behavior: auto !important;
433
+ }
434
+ }
435
+
436
+ /* 触屏设备(无精细指针):关闭 hover 抬升、压低 pressable scale */
437
+ @media (pointer: coarse) {
438
+ .lift:hover { transform: none; }
439
+ .pressable:active { transform: translateY(0) scale(0.99); }
440
+ }
441
+
442
+ /* 高对比度模式(Windows High Contrast / 强制 colors) */
443
+ @media (forced-colors: active) {
444
+ .pressable, .control, .lift, .surface-elevated, .focus-ring:focus-visible {
445
+ forced-color-adjust: none;
446
+ }
447
+ .focus-ring:focus-visible,
448
+ .pressable:focus-visible {
449
+ outline: 2px solid Highlight;
450
+ outline-offset: 2px;
451
+ }
452
+ }
453
+
454
+ /* 打印:去掉阴影/背景/动效,节约墨水并避免打印截断 */
455
+ @media print {
456
+ *, *::before, *::after {
457
+ background: transparent !important;
458
+ color: #000 !important;
459
+ box-shadow: none !important;
460
+ text-shadow: none !important;
461
+ }
462
+ body { font-size: 12pt; line-height: 1.4; }
463
+ a, a:visited { text-decoration: underline; }
464
+ a[href]::after { content: ' (' attr(href) ')'; font-size: 90%; }
465
+ pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
466
+ h1, h2, h3 { page-break-after: avoid; }
467
+ .pressable, .lift, .surface-elevated { box-shadow: none !important; transform: none !important; }
127
468
  }
@@ -11,6 +11,7 @@ function setup(container) {
11
11
  if (!trigger) return;
12
12
  const item = trigger.closest(".accordion-item");
13
13
  if (!item || trigger.closest(".accordion") !== container) return;
14
+ if (item.classList.contains("is-disabled")) return;
14
15
  const open = item.classList.toggle("is-open");
15
16
  trigger.setAttribute("aria-expanded", String(open));
16
17
  if (single && open) {
@@ -21,6 +22,25 @@ function setup(container) {
21
22
  });
22
23
  }
23
24
  });
25
+ container.addEventListener("keydown", (ev) => {
26
+ const kev = ev;
27
+ const target = kev.target;
28
+ if (!(target instanceof Element)) return;
29
+ const trigger = target.closest(".accordion-trigger");
30
+ if (!trigger || trigger.closest(".accordion") !== container) return;
31
+ const triggers = Array.from(
32
+ container.querySelectorAll(".accordion-item:not(.is-disabled) > .accordion-trigger")
33
+ );
34
+ const idx = triggers.indexOf(trigger);
35
+ if (idx === -1) return;
36
+ if (kev.key === "Home") {
37
+ kev.preventDefault();
38
+ triggers[0]?.focus();
39
+ } else if (kev.key === "End") {
40
+ kev.preventDefault();
41
+ triggers[triggers.length - 1]?.focus();
42
+ }
43
+ });
24
44
  }
25
45
  function initAccordion(root = document) {
26
46
  if (typeof document === "undefined") return;