@icen.ai/ui 0.2.1 → 0.4.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.
- package/README.md +29 -16
- package/dist/base.css +389 -48
- package/dist/behaviors/accordion.mjs +20 -0
- package/dist/behaviors/carousel.mjs +30 -1
- package/dist/behaviors/charts.mjs +356 -0
- package/dist/behaviors/copy.mjs +4 -3
- package/dist/behaviors/datatable.mjs +968 -0
- package/dist/behaviors/input.mjs +20 -1
- package/dist/behaviors/modal.mjs +14 -0
- package/dist/behaviors/nav.mjs +31 -4
- package/dist/behaviors/sidebar.mjs +12 -0
- package/dist/behaviors/slider.mjs +35 -3
- package/dist/behaviors/tabs.mjs +100 -9
- package/dist/behaviors/tag-input.mjs +11 -0
- package/dist/behaviors/theme.mjs +26 -0
- package/dist/behaviors/toast.mjs +83 -15
- package/dist/behaviors/tree.mjs +57 -1
- package/dist/behaviors/upload.mjs +2 -3
- package/dist/cli.mjs +74 -0
- package/dist/components/accordion.mjs +3 -0
- package/dist/components/alert.mjs +2 -0
- package/dist/components/avatar.mjs +2 -0
- package/dist/components/breadcrumb.css +91 -18
- package/dist/components/breadcrumb.mjs +2 -0
- package/dist/components/btn.css +174 -14
- package/dist/components/btn.mjs +2 -0
- package/dist/components/card.css +118 -18
- package/dist/components/card.mjs +2 -0
- package/dist/components/carousel.mjs +3 -0
- package/dist/components/chart-area.css +15 -0
- package/dist/components/chart-area.mjs +4 -0
- package/dist/components/chart-bar.css +79 -0
- package/dist/components/chart-bar.mjs +4 -0
- package/dist/components/chart-gauge.css +38 -0
- package/dist/components/chart-gauge.mjs +4 -0
- package/dist/components/chart-heatmap.css +52 -0
- package/dist/components/chart-heatmap.mjs +4 -0
- package/dist/components/chart-line.css +15 -0
- package/dist/components/chart-line.mjs +4 -0
- package/dist/components/chart-pie.css +26 -0
- package/dist/components/chart-pie.mjs +4 -0
- package/dist/components/chart-radar.css +19 -0
- package/dist/components/chart-radar.mjs +4 -0
- package/dist/components/chart-sparkline.css +14 -0
- package/dist/components/chart-sparkline.mjs +4 -0
- package/dist/components/chart-stack.css +30 -0
- package/dist/components/chart-stack.mjs +4 -0
- package/dist/components/charts.css +66 -74
- package/dist/components/charts.mjs +12 -0
- package/dist/components/content.css +256 -74
- package/dist/components/context-menu.mjs +3 -0
- package/dist/components/datatable.css +429 -0
- package/dist/components/datatable.mjs +4 -0
- package/dist/components/desc.mjs +2 -0
- package/dist/components/dropdown.mjs +3 -0
- package/dist/components/empty.css +50 -10
- package/dist/components/empty.mjs +2 -0
- package/dist/components/feedback.css +158 -47
- package/dist/components/form.css +139 -22
- package/dist/components/input.css +165 -45
- package/dist/components/input.mjs +3 -0
- package/dist/components/kbd.mjs +2 -0
- package/dist/components/layout.mjs +2 -0
- package/dist/components/list.mjs +2 -0
- package/dist/components/media-card.mjs +2 -0
- package/dist/components/media.css +187 -40
- package/dist/components/menu.css +159 -32
- package/dist/components/modal.css +140 -34
- package/dist/components/modal.mjs +3 -0
- package/dist/components/nav.css +134 -32
- package/dist/components/nav.mjs +3 -0
- package/dist/components/pagination.css +105 -19
- package/dist/components/pagination.mjs +2 -0
- package/dist/components/panel.css +61 -6
- package/dist/components/panel.mjs +2 -0
- package/dist/components/pill.css +74 -14
- package/dist/components/pill.mjs +2 -0
- package/dist/components/popover.css +109 -7
- package/dist/components/popover.mjs +3 -0
- package/dist/components/progress.mjs +2 -0
- package/dist/components/rating.mjs +2 -0
- package/dist/components/result.mjs +2 -0
- package/dist/components/segmented.css +199 -50
- package/dist/components/segmented.mjs +2 -0
- package/dist/components/select.css +140 -39
- package/dist/components/select.mjs +3 -0
- package/dist/components/sidebar.css +162 -34
- package/dist/components/sidebar.mjs +3 -0
- package/dist/components/skeleton.mjs +2 -0
- package/dist/components/slider.css +169 -31
- package/dist/components/slider.mjs +3 -0
- package/dist/components/spinner.mjs +2 -0
- package/dist/components/stat.css +103 -8
- package/dist/components/stat.mjs +2 -0
- package/dist/components/steps.css +105 -31
- package/dist/components/steps.mjs +2 -0
- package/dist/components/switch.css +171 -54
- package/dist/components/switch.mjs +2 -0
- package/dist/components/table.css +114 -12
- package/dist/components/table.mjs +2 -0
- package/dist/components/tabs.css +104 -17
- package/dist/components/tabs.mjs +3 -0
- package/dist/components/tag-input.css +119 -23
- package/dist/components/tag-input.mjs +3 -0
- package/dist/components/timeline.mjs +2 -0
- package/dist/components/toast.css +85 -12
- package/dist/components/toast.mjs +3 -0
- package/dist/components/tooltip.css +86 -26
- package/dist/components/tooltip.mjs +2 -0
- package/dist/components/tree.mjs +3 -0
- package/dist/components/upload.css +144 -17
- package/dist/components/upload.mjs +3 -0
- package/dist/index.mjs +811 -42
- package/dist/registry.json +310 -2
- package/dist/retro-effects.css +264 -0
- package/dist/tokens/colors.css +18 -90
- package/dist/tokens/retro-effects.css +264 -0
- package/dist/tokens/style-profiles.css +47 -47
- package/dist/tokens.css +65 -137
- package/dist/ui.css +5069 -1026
- 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 色彩预设 × 明暗双模 ×
|
|
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
|
|
7
|
-
-
|
|
8
|
-
-
|
|
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 {
|
|
27
|
-
initTheme
|
|
28
|
-
initTabs
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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 预设 ×
|
|
57
|
-
|
|
58
|
-
|
|
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/
|
|
61
|
-
src/
|
|
62
|
-
|
|
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
|
-
*
|
|
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 {
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
38
|
-
|
|
39
|
-
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
/*
|
|
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-
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
275
|
+
/* ── .pressable — 可按压(按钮、卡片按钮、tag close 等)
|
|
276
|
+
hover:顶部高光 ::after 渐入;focus-visible:同样高光 + 描边
|
|
277
|
+
active:下陷 + 微缩;aria-busy:减淡;disabled / aria-disabled:50% + 禁手势 */
|
|
71
278
|
.pressable {
|
|
72
|
-
position: relative;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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: '';
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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,
|
|
325
|
+
.pressable:disabled::after,
|
|
326
|
+
.pressable[aria-disabled='true']::after,
|
|
327
|
+
.pressable[data-disabled]::after { display: none; }
|
|
92
328
|
|
|
93
|
-
/*
|
|
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(--
|
|
96
|
-
border-color var(--
|
|
97
|
-
box-shadow var(--
|
|
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(--
|
|
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(--
|
|
106
|
-
box-shadow var(--
|
|
107
|
-
transform var(--
|
|
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
|
-
/*
|
|
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:
|
|
414
|
+
box-shadow: var(--icen-ring);
|
|
119
415
|
}
|
|
120
416
|
|
|
121
|
-
/*
|
|
417
|
+
/* ── .motion — 显式动画工具类(仅在 prefers-reduced-motion: no-preference 时生效) */
|
|
122
418
|
@media (prefers-reduced-motion: no-preference) {
|
|
123
|
-
.motion { transition: all var(--
|
|
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 {
|
|
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;
|