@icen.ai/ui 0.1.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 ADDED
@@ -0,0 +1,58 @@
1
+ # @icen.ai/ui — Icen Design System
2
+
3
+ icen.ai 全生态的统一设计系统:**设计 tokens(6 色彩预设 × 明暗双模 × 风格配置)+ 无框架组件 CSS + 行为 JS**。工程形态与 `@icen.ai/cli` 一致(Bun + TS + tsup + ESM + MIT)。
4
+
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`
9
+
10
+ ## 使用
11
+
12
+ ```bash
13
+ bun add @icen.ai/ui
14
+ ```
15
+
16
+ ```css
17
+ @import '@icen.ai/ui/tokens.css';
18
+ @import '@icen.ai/ui/ui.css'; /* tokens + base + 全部组件;也可按 ./components/btn.css 单引 */
19
+ ```
20
+
21
+ ```html
22
+ <html class="piano dark"> <!-- 预设类 + 可选 dark;clay 为 :root 默认 -->
23
+ ```
24
+
25
+ ```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('已保存');
31
+ ```
32
+
33
+ ## 构建
34
+
35
+ ```bash
36
+ bun install
37
+ bun run build # tsup(behaviors → dist/*.mjs)+ scripts/build-css.ts(css → dist/)
38
+ ```
39
+
40
+ 产物:`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`。
41
+
42
+ ## 目录
43
+
44
+ ```
45
+ src/tokens/colors.css # 6 预设 × 明暗(移植自 opengal 的 Icen Design System;
46
+ # clay 基值已原地校准到 accounts.icen.ai 视觉)
47
+ src/tokens/style-profiles.css # density/motion/typography + .style-*
48
+ src/tokens/typography.css # icen canonical 字体栈(IBM Plex Mono + CJK 宋体)
49
+ src/base.css # 元素基线 + 无箭头细滚动条(全部消费 token)
50
+ src/components/*.css # 8 个无框架组件
51
+ src/behaviors/*.ts # theme / tabs / toast / copy
52
+ scripts/build-css.ts # CSS 产物构建(拼合 + registry.json)
53
+ site/ # 文档站骨架(ui.icen.ai,独立 Astro 应用)
54
+ ```
55
+
56
+ ## 消费方接入(accounts 模式)
57
+
58
+ accounts 目前以 **vendor** 方式消费:`bun run sync:ui`(accounts 仓库内)把本仓库 `dist/` 复制到 `src/styles/vendor/icen/`,布局里 import 对应 CSS/JS。npm 发布后可改为包引入。
package/dist/base.css ADDED
@@ -0,0 +1,55 @@
1
+ /*
2
+ * @icen.ai/ui — Base(元素基线 + 滚动条)
3
+ * 全部消费 --token-*;任何 preset/明暗/style profile 下自洽。
4
+ * 在 tokens 之后、components 之前引入。
5
+ */
6
+
7
+ *, *::before, *::after { box-sizing: border-box; }
8
+
9
+ html, body { margin: 0; padding: 0; }
10
+
11
+ body {
12
+ background: var(--token-bg);
13
+ color: var(--token-text);
14
+ font-size: 14px;
15
+ min-height: 100vh;
16
+ }
17
+
18
+ a { color: var(--token-accent); text-decoration: none; }
19
+ a:hover { text-decoration: underline; }
20
+
21
+ code, pre {
22
+ font-family: var(--font-mono);
23
+ background: var(--token-bg-soft);
24
+ border-radius: var(--radius-xs);
25
+ }
26
+ code { padding: 1px 5px; }
27
+ pre { padding: 12px 14px; overflow-x: auto; }
28
+ pre code { padding: 0; background: none; }
29
+
30
+ button { font: inherit; }
31
+ img { max-width: 100%; }
32
+
33
+ ::selection {
34
+ background: color-mix(in srgb, var(--token-accent) 25%, transparent);
35
+ }
36
+
37
+ /* ── 全局滚动条:细薄、无上下箭头、贴 token ── */
38
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
39
+ ::-webkit-scrollbar-track { background: transparent; }
40
+ ::-webkit-scrollbar-thumb {
41
+ background: color-mix(in srgb, var(--token-text) 16%, transparent);
42
+ border-radius: 999px;
43
+ }
44
+ ::-webkit-scrollbar-thumb:hover {
45
+ background: color-mix(in srgb, var(--token-accent) 50%, transparent);
46
+ }
47
+ ::-webkit-scrollbar-button,
48
+ ::-webkit-scrollbar-button:single-button { display: none; width: 0; height: 0; }
49
+ ::-webkit-scrollbar-corner { background: transparent; }
50
+ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-text) 16%, transparent) transparent; }
51
+
52
+ /* ── 通用工具类 ── */
53
+ .mono { font-family: var(--font-mono); }
54
+ .dim { color: var(--token-text-muted); }
55
+ .faint { color: var(--token-text-faint); }
@@ -0,0 +1,62 @@
1
+ // src/behaviors/copy.ts
2
+ var initialized = /* @__PURE__ */ new WeakSet();
3
+ var flashing = /* @__PURE__ */ new WeakMap();
4
+ function legacyCopy(text) {
5
+ const ta = document.createElement("textarea");
6
+ ta.value = text;
7
+ ta.setAttribute("readonly", "");
8
+ ta.style.position = "fixed";
9
+ ta.style.opacity = "0";
10
+ document.body.appendChild(ta);
11
+ ta.select();
12
+ let ok = false;
13
+ try {
14
+ ok = document.execCommand("copy");
15
+ } catch {
16
+ ok = false;
17
+ }
18
+ ta.remove();
19
+ return ok;
20
+ }
21
+ async function copyText(text) {
22
+ try {
23
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
24
+ await navigator.clipboard.writeText(text);
25
+ return true;
26
+ }
27
+ } catch {
28
+ }
29
+ return legacyCopy(text);
30
+ }
31
+ function flashDone(btn) {
32
+ const prev = flashing.get(btn);
33
+ if (prev) window.clearTimeout(prev.timer);
34
+ const original = prev ? prev.original : btn.textContent;
35
+ btn.textContent = "\u5DF2\u590D\u5236";
36
+ btn.classList.add("done");
37
+ const timer = window.setTimeout(() => {
38
+ btn.textContent = original;
39
+ btn.classList.remove("done");
40
+ flashing.delete(btn);
41
+ }, 1400);
42
+ flashing.set(btn, { original, timer });
43
+ }
44
+ function onClick(root, ev) {
45
+ const target = ev.target;
46
+ if (!(target instanceof Element)) return;
47
+ const btn = target.closest(".copy-btn[data-copy]");
48
+ if (!btn || !root.contains(btn)) return;
49
+ const text = btn.getAttribute("data-copy") ?? "";
50
+ void copyText(text).then((ok) => {
51
+ if (ok) flashDone(btn);
52
+ });
53
+ }
54
+ function initCopy(root = document) {
55
+ if (typeof document === "undefined") return;
56
+ if (initialized.has(root)) return;
57
+ initialized.add(root);
58
+ root.addEventListener("click", (ev) => onClick(root, ev));
59
+ }
60
+ export {
61
+ initCopy
62
+ };
@@ -0,0 +1,63 @@
1
+ // src/behaviors/tabs.ts
2
+ function tabNames(container) {
3
+ return Array.from(container.querySelectorAll("[data-tab]")).map((t) => t.getAttribute("data-tab")).filter((n) => n !== null);
4
+ }
5
+ function currentHash() {
6
+ return window.location.hash.replace(/^#/, "");
7
+ }
8
+ function activate(container, name, writeHash) {
9
+ let matched = false;
10
+ container.querySelectorAll("[data-tab]").forEach((t) => {
11
+ const on = t.getAttribute("data-tab") === name;
12
+ if (on) matched = true;
13
+ t.classList.toggle("active", on);
14
+ });
15
+ if (!matched) return;
16
+ container.querySelectorAll("[data-tab-panel]").forEach((p) => {
17
+ p.hidden = p.getAttribute("data-tab-panel") !== name;
18
+ });
19
+ if (writeHash && typeof window !== "undefined") {
20
+ window.history.replaceState(null, "", `#${name}`);
21
+ }
22
+ }
23
+ function setup(container) {
24
+ const el = container;
25
+ if (el.__icenTabsInit) return;
26
+ el.__icenTabsInit = true;
27
+ const useHash = container.hasAttribute("data-tabs-hash");
28
+ const names = tabNames(container);
29
+ if (names.length === 0) return;
30
+ container.addEventListener("click", (ev) => {
31
+ const target = ev.target;
32
+ if (!(target instanceof Element)) return;
33
+ const btn = target.closest("[data-tab]");
34
+ if (!btn || btn.closest("[data-tabs]") !== container) return;
35
+ const name = btn.getAttribute("data-tab");
36
+ if (name) activate(container, name, useHash);
37
+ });
38
+ let initial;
39
+ if (useHash) {
40
+ const h = currentHash();
41
+ if (names.includes(h)) initial = h;
42
+ } else {
43
+ const n = container.querySelector("[data-tab].active")?.getAttribute("data-tab");
44
+ if (n && names.includes(n)) initial = n;
45
+ }
46
+ activate(container, initial ?? names[0], false);
47
+ if (useHash && typeof window !== "undefined") {
48
+ window.addEventListener("hashchange", () => {
49
+ const h = currentHash();
50
+ if (names.includes(h)) activate(container, h, false);
51
+ });
52
+ }
53
+ }
54
+ function initTabs(root = document) {
55
+ if (typeof document === "undefined") return;
56
+ const containers = [];
57
+ if (root instanceof Element && root.matches("[data-tabs]")) containers.push(root);
58
+ containers.push(...Array.from(root.querySelectorAll("[data-tabs]")));
59
+ for (const c of containers) setup(c);
60
+ }
61
+ export {
62
+ initTabs
63
+ };
@@ -0,0 +1,80 @@
1
+ // src/behaviors/theme.ts
2
+ var PRESETS = ["clay", "piano", "art", "vangogh", "ink", "retro"];
3
+ var STYLES = ["modern", "retro", "terminal"];
4
+ var STORAGE_KEY = "icen.ui.theme";
5
+ var FALLBACK = { preset: "clay", dark: false, style: "modern" };
6
+ function isBrowser() {
7
+ return typeof document !== "undefined";
8
+ }
9
+ function getDefaultTheme() {
10
+ if (!isBrowser() || typeof window.matchMedia !== "function") {
11
+ return { ...FALLBACK };
12
+ }
13
+ return {
14
+ preset: "clay",
15
+ dark: window.matchMedia("(prefers-color-scheme: dark)").matches,
16
+ style: "modern"
17
+ };
18
+ }
19
+ function isThemeState(v) {
20
+ if (typeof v !== "object" || v === null) return false;
21
+ const t = v;
22
+ return typeof t.preset === "string" && PRESETS.includes(t.preset) && typeof t.dark === "boolean" && typeof t.style === "string" && STYLES.includes(t.style);
23
+ }
24
+ function getTheme() {
25
+ if (!isBrowser()) return getDefaultTheme();
26
+ try {
27
+ const raw = window.localStorage.getItem(STORAGE_KEY);
28
+ if (!raw) return getDefaultTheme();
29
+ const parsed = JSON.parse(raw);
30
+ if (!isThemeState(parsed)) return getDefaultTheme();
31
+ return { preset: parsed.preset, dark: parsed.dark, style: parsed.style };
32
+ } catch {
33
+ return getDefaultTheme();
34
+ }
35
+ }
36
+ function applyTheme(t) {
37
+ if (!isBrowser()) return;
38
+ const cls = document.documentElement.classList;
39
+ for (const p of PRESETS) cls.remove(p);
40
+ for (const s of STYLES) cls.remove(`style-${s}`);
41
+ cls.remove("dark");
42
+ cls.add(t.preset);
43
+ if (t.dark) cls.add("dark");
44
+ cls.add(`style-${t.style}`);
45
+ }
46
+ function setTheme(patch) {
47
+ const merged = { ...getTheme(), ...patch };
48
+ const next = {
49
+ preset: PRESETS.includes(merged.preset) ? merged.preset : FALLBACK.preset,
50
+ dark: merged.dark === true,
51
+ style: STYLES.includes(merged.style) ? merged.style : FALLBACK.style
52
+ };
53
+ applyTheme(next);
54
+ if (isBrowser()) {
55
+ try {
56
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
57
+ } catch {
58
+ }
59
+ }
60
+ return next;
61
+ }
62
+ function toggleDark() {
63
+ return setTheme({ dark: !getTheme().dark });
64
+ }
65
+ function initTheme() {
66
+ const t = getTheme();
67
+ applyTheme(t);
68
+ return t;
69
+ }
70
+ export {
71
+ PRESETS,
72
+ STORAGE_KEY,
73
+ STYLES,
74
+ applyTheme,
75
+ getDefaultTheme,
76
+ getTheme,
77
+ initTheme,
78
+ setTheme,
79
+ toggleDark
80
+ };
@@ -0,0 +1,35 @@
1
+ // src/behaviors/toast.ts
2
+ function ensureContainer() {
3
+ const existing = document.body.querySelector(".toast-container");
4
+ if (existing) return existing;
5
+ const el = document.createElement("div");
6
+ el.className = "toast-container";
7
+ document.body.appendChild(el);
8
+ return el;
9
+ }
10
+ function show(message, kind = "info", ms = 2600) {
11
+ if (typeof document === "undefined") return;
12
+ const el = document.createElement("div");
13
+ el.className = kind === "info" ? "toast" : `toast ${kind}`;
14
+ el.textContent = message;
15
+ ensureContainer().appendChild(el);
16
+ let removed = false;
17
+ const remove = () => {
18
+ if (removed) return;
19
+ removed = true;
20
+ el.remove();
21
+ };
22
+ window.setTimeout(() => {
23
+ el.classList.add("leaving");
24
+ el.addEventListener("transitionend", remove, { once: true });
25
+ window.setTimeout(remove, 400);
26
+ }, ms);
27
+ }
28
+ var toast = Object.assign(show, {
29
+ ok: (message, ms) => show(message, "ok", ms),
30
+ err: (message, ms) => show(message, "err", ms),
31
+ warn: (message, ms) => show(message, "warn", ms)
32
+ });
33
+ export {
34
+ toast
35
+ };
@@ -0,0 +1,23 @@
1
+ /*
2
+ * @icen.ai/ui — Component: btn(按钮)
3
+ * 用法:<button class="btn btn-primary"> / .btn-danger / .btn-sm / .btn-block
4
+ */
5
+
6
+ .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);
12
+ }
13
+ .btn:hover { border-color: var(--token-accent); }
14
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
15
+
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); }
18
+
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); }
21
+
22
+ .btn-block { display: block; width: 100%; text-align: center; }
23
+ .btn-sm { padding: 6px 14px; font-size: 12px; }
@@ -0,0 +1,31 @@
1
+ /*
2
+ * @icen.ai/ui — Component: form(表单字段/输入/错误与成功提示)
3
+ */
4
+
5
+ .form-field { margin-bottom: 18px; }
6
+ .form-label { display: block; font-size: 12px; color: var(--token-text-muted); margin-bottom: 6px; letter-spacing: 1px; }
7
+ .form-input {
8
+ width: 100%; padding: 11px 14px;
9
+ background: var(--token-bg); color: var(--token-text);
10
+ border: 1px solid var(--token-line-soft); border-radius: var(--radius-sm);
11
+ font-family: var(--font-mono); font-size: 14px;
12
+ outline: none; transition: border-color var(--duration-fast) var(--ease-out);
13
+ }
14
+ .form-input:focus { border-color: var(--token-accent); }
15
+ .form-input::placeholder { color: var(--token-text-faint); }
16
+
17
+ .form-error {
18
+ background: color-mix(in srgb, var(--token-error) 7%, transparent); color: var(--token-error);
19
+ border: 1px solid color-mix(in srgb, var(--token-error) 25%, transparent); border-radius: var(--radius-sm);
20
+ padding: 10px 14px; font-size: 12.5px; margin-bottom: 16px;
21
+ font-family: var(--font-heading);
22
+ }
23
+ .form-ok {
24
+ background: color-mix(in srgb, var(--token-success) 8%, transparent); color: var(--token-success);
25
+ border: 1px solid color-mix(in srgb, var(--token-success) 25%, transparent); border-radius: var(--radius-sm);
26
+ padding: 10px 14px; font-size: 12.5px; margin-bottom: 16px;
27
+ font-family: var(--font-heading);
28
+ }
29
+
30
+ /* 页面级横幅(与表单提示同族,用于操作结果/引导提示) */
31
+ .banner-ok, .banner-err { margin-bottom: 20px; }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * @icen.ai/ui — Component: panel(白卡片容器 + 标题栏)
3
+ * 用法:<div class="panel-block"><div class="panel-title"><span>标题</span></div> …内容… </div>
4
+ */
5
+
6
+ .panel-block {
7
+ background: var(--token-surface);
8
+ border: 1px solid var(--token-line-soft);
9
+ border-radius: var(--radius-lg);
10
+ box-shadow: var(--token-shadow-elevated);
11
+ margin-bottom: 24px;
12
+ overflow: hidden;
13
+ }
14
+ .panel-title {
15
+ display: flex; align-items: center; justify-content: space-between;
16
+ padding: 14px 20px; border-bottom: 1px solid var(--token-line-soft);
17
+ font-size: 12px; letter-spacing: 2px; color: var(--token-text-muted);
18
+ text-transform: uppercase;
19
+ }
@@ -0,0 +1,28 @@
1
+ /*
2
+ * @icen.ai/ui — Component: pill(状态徽章)
3
+ * 用法:<span class="pill ok|warn|bad|accent">文本</span>
4
+ * 语义色纪律:仅绿/黄/红 + 品牌 accent。
5
+ */
6
+
7
+ .pill {
8
+ display: inline-block; padding: 2px 10px; border-radius: var(--radius-full);
9
+ font-size: 11px; letter-spacing: 1px;
10
+ background: var(--token-surface-strong); color: var(--token-text-muted);
11
+ border: 1px solid var(--token-line-soft);
12
+ }
13
+ .pill.ok {
14
+ color: var(--token-success); border-color: color-mix(in srgb, var(--token-success) 35%, transparent);
15
+ background: color-mix(in srgb, var(--token-success) 7%, transparent);
16
+ }
17
+ .pill.warn {
18
+ color: var(--token-warning); border-color: color-mix(in srgb, var(--token-warning) 35%, transparent);
19
+ background: color-mix(in srgb, var(--token-warning) 7%, transparent);
20
+ }
21
+ .pill.bad {
22
+ color: var(--token-error); border-color: color-mix(in srgb, var(--token-error) 35%, transparent);
23
+ background: color-mix(in srgb, var(--token-error) 7%, transparent);
24
+ }
25
+ .pill.accent {
26
+ color: var(--token-accent-deep); border-color: color-mix(in srgb, var(--token-accent) 40%, transparent);
27
+ background: color-mix(in srgb, var(--token-accent) 7%, transparent);
28
+ }
@@ -0,0 +1,18 @@
1
+ /*
2
+ * @icen.ai/ui — Component: stat(指标卡)
3
+ * 用法:<div class="stat-grid"><div class="stat-card"><div class="stat-num">0</div><div class="stat-label">用户总数</div></div>…
4
+ */
5
+
6
+ .stat-grid {
7
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
8
+ gap: 14px; margin-bottom: 24px;
9
+ }
10
+ .stat-card {
11
+ background: var(--token-surface); border: 1px solid var(--token-line-soft);
12
+ border-radius: var(--radius-lg); padding: 14px 16px;
13
+ box-shadow: var(--token-shadow-elevated);
14
+ }
15
+ .stat-num { font-size: 24px; font-weight: 700; line-height: 1.25; word-break: break-all; }
16
+ .stat-num.accent { color: var(--token-accent); }
17
+ .stat-num.warn { color: var(--token-warning); }
18
+ .stat-label { color: var(--token-text-faint); font-size: 11px; letter-spacing: 1px; margin-top: 2px; }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * @icen.ai/ui — Component: table(数据表)
3
+ * 用法:<div class="table-wrap"><table class="admin-table">…
4
+ * .table-wrap 负责窄屏横向滚动(滚动条样式见 base.css);行内操作列用 .ops。
5
+ */
6
+
7
+ .table-wrap { overflow-x: auto; }
8
+ .admin-table { width: 100%; border-collapse: collapse; font-size: 13px; }
9
+ .admin-table th {
10
+ text-align: left; font-weight: 400; font-size: 11px; letter-spacing: 1px;
11
+ color: var(--token-text-faint); padding: 10px 14px;
12
+ border-bottom: 1px solid var(--token-line-soft); white-space: nowrap;
13
+ }
14
+ .admin-table td { padding: 10px 14px; border-bottom: 1px solid var(--token-line-soft); vertical-align: middle; }
15
+ .admin-table tbody tr:last-child td { border-bottom: 0; }
16
+ .admin-table tbody tr:hover { background: var(--token-surface-strong); }
17
+ .admin-table tbody tr.row-link { cursor: pointer; }
18
+ .admin-table .ops { white-space: nowrap; text-align: right; }
19
+ .admin-table .ops .btn-sm { margin-left: 6px; }
@@ -0,0 +1,26 @@
1
+ /*
2
+ * @icen.ai/ui — Component: tabs(分区导航)
3
+ * .page-tabs > .page-tab(页面内分区,active 加粗)
4
+ * .admin-tabs > a(顶部子导航)
5
+ * 窄屏换行不滚动(overflow 纪律:tab 条永不出现滚动条)。
6
+ */
7
+
8
+ .page-tabs, .admin-tabs {
9
+ display: flex; gap: 2px; flex-wrap: wrap;
10
+ border-bottom: 1px solid var(--token-line-soft); margin-bottom: 26px;
11
+ }
12
+ .page-tab {
13
+ background: none; border: 0; cursor: pointer;
14
+ padding: 9px 14px; font-family: var(--font-mono); font-size: 13px; letter-spacing: 1px;
15
+ color: var(--token-text-muted); border-bottom: 2px solid transparent;
16
+ margin-bottom: -1px; white-space: nowrap;
17
+ }
18
+ .page-tab:hover { color: var(--token-accent); }
19
+ .page-tab.active { color: var(--token-accent); border-bottom-color: var(--token-accent); font-weight: 700; }
20
+
21
+ .admin-tabs a {
22
+ padding: 9px 14px; font-size: 13px; color: var(--token-text-muted);
23
+ border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
24
+ }
25
+ .admin-tabs a:hover { color: var(--token-accent); text-decoration: none; }
26
+ .admin-tabs a.active { color: var(--token-accent); border-bottom-color: var(--token-accent); }
@@ -0,0 +1,27 @@
1
+ /*
2
+ * @icen.ai/ui — Component: toast(轻提示,与 behaviors/toast 配套)
3
+ * 容器固定在右下角,左缘 3px 语义色条;滑入动画用 motion token。
4
+ */
5
+
6
+ .toast-container {
7
+ position: fixed; right: 20px; bottom: 20px; z-index: 9999;
8
+ display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
9
+ pointer-events: none;
10
+ }
11
+ .toast {
12
+ pointer-events: auto;
13
+ background: var(--token-surface); color: var(--token-text);
14
+ border: 1px solid var(--token-line-soft); border-left: 3px solid var(--token-line-strong);
15
+ border-radius: var(--radius-sm); box-shadow: var(--token-shadow-overlay);
16
+ padding: 11px 16px; font-size: 13px; max-width: 360px;
17
+ animation: icen-toast-in var(--duration-slow) var(--ease-out);
18
+ }
19
+ .toast.ok { border-left-color: var(--token-success); }
20
+ .toast.err { border-left-color: var(--token-error); }
21
+ .toast.warn { border-left-color: var(--token-warning); }
22
+ .toast.leaving { opacity: 0; transform: translateY(8px); transition: opacity var(--duration-normal) var(--ease-out), transform var(--duration-normal) var(--ease-out); }
23
+
24
+ @keyframes icen-toast-in {
25
+ from { opacity: 0; transform: translateY(8px); }
26
+ to { opacity: 1; transform: translateY(0); }
27
+ }