Files
AgentLab/frontend/src/index.css
T
ThearchyHelios 78800511e0 前端:分析卡节点(分析能力一轮)。节点库「把关」一类加「分析卡」(类型色 --nt-analysis,亮暗两套),卡片摘要写做哪几种分析(「波动基线、变化拆解(可加)」,叫法照契约);检查器的分析项编辑器:每项标识、分析方法(五个算子)、模式(有模式的才给)、输入只列上游的查询节点(调用数据库查询工具的「调用工具」节点、合并查询,删掉或挪到下游的照实列出),按方法摆参数格(analysisItems.paramSpecs:维度列、期次、基期、本期、度量列或分子分母、乘积和因子、比较方式、粒度、对照组、几对……,期次可以写 {{ }}),可加、比率的变化拆解另有总数核对两格和下钻(上一层只列排在前面的变化拆解),变化拆解、可比性检查可以写成基期、本期两份查询,换方法、模式时只留新方法用得到的参数;在验证什么一句话;阈值覆盖只列写了的几项、从没写的里选,范围当场按契约说(两两之间的大小交给服务端),全删掉不留空对象。服务端按 items[i].<键> 报的问题落在那一格下面,定位退不到的子键(params.parent.key)逐段往上退;查询节点连到分析卡时还没选输入的第一项选上它(同合并查询,和连线同一步撤销)。报告节点加「分析说法核对」(缺省重写一次后标出)、「分析范围」(高级,缺省有分析结论时附上),选项以 engine/labels.py 的短说法开头。叫法、范围只照契约一份(lib/termsAnalysis:OP_LABELS / MODE_LABELS / LABELS / RANGES,check-studio 读后端源码逐项比对)。check-studio 加两节:叫法与契约一致、分析卡的画布编辑与校验定位
2026-10-21 18:17:56 +08:00

1927 lines
59 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@import 'tailwindcss';
@import '@xyflow/react/dist/style.css';
/* ==========================================================================
主题 token。深色是默认(长时间盯画布,深色更省眼),浅色由 data-theme 切换;
没有 data-theme 就是「跟随系统」,由下面的 prefers-color-scheme 分支接管。
index.html 在首帧前从 localStorage 读偏好写 data-theme(见 lib/theme.ts),
所以不会先闪一帧深色再跳浅色。
对比度都是按 WCAG 公式实算过的:
- 文字三级 text / text-dim / text-faint 在 bg、panel、hover 三种底色上都
≥ 4.5:1(faint 以前只有 2.7–3.6,中文小字在投影和强光下读不清);
- --accent-solid / --warn-solid / --err-solid 是给「实心底 + 字」用的,
上面的字用配对的 --on-*,同样 ≥ 4.5:1。--accent 本身留给描边、文字和图形,
深色下白字压在 #4f8cff 上只有 3.2:1;
- 状态色、节点类型色作为图形都 ≥ 3:1。
颜色的分工(监控界面的通行做法:常态用中性色,颜色留给状态):
- ok / warn / err / accent 只表达状态(accent 兼管「选中 / 焦点 / 主操作」),
状态一律走下面的 --st-* 语义令牌;
- 节点类型色 --nt-* 只在青—蓝灰—紫—品红这一段取色,暖色和强调蓝都让给
状态,免得「人工审批」平时就是红的、像失败了。
浅色这一套写了两遍(data-theme='light' 和「跟随系统 + 系统是浅色」),
CSS 没法把选择器和媒体查询并成一条规则。改一处必须同步改另一处。
========================================================================== */
:root {
color-scheme: dark;
--bg: #0b0e14;
--bg-elev: #11151f;
--bg-panel: #141926;
--bg-hover: #1b2232;
--border: #232a3a;
--border-strong: #333c52;
--text: #e4e8f1;
--text-dim: #a0abc1;
--text-faint: #7f8ba4;
--accent: #4f8cff;
--accent-soft: #15284d;
--accent-solid: #336cf0;
--on-accent: #ffffff;
--ok: #3fb950;
--warn: #d29922;
--warn-solid: #d29922;
--on-warn: #1a1300;
--err: #f85149;
--err-solid: #da3633;
--on-err: #ffffff;
--copilot: #bc8cff;
/* 画布底纹两级:细点每格一个,粗点每 5 格一个,像工程图纸的坐标纸 */
--grid-dot: #2a3346;
--grid-dot-major: #3a445b;
--hairline: #1c2230;
--elev-1: 0 1px 2px rgb(0 0 0 / 0.35);
--elev-2: 0 6px 16px -4px rgb(0 0 0 / 0.55), 0 2px 4px -2px rgb(0 0 0 / 0.45);
--elev-3: 0 20px 48px -12px rgb(0 0 0 / 0.7), 0 8px 16px -8px rgb(0 0 0 / 0.5);
/* 推导出来的状态:比基础状态色压一档,只在图形 + 剪影里出现 */
--st-queued: #678cd2;
--st-suspended: #b4a481;
--st-blocked: #bc6e77;
--st-unreached: #68738b;
/* 节点类型色。按类别分色族:起止石板灰、模型紫、执行青绿、控制品红、
上下文天青、把关兰紫;族内靠明度区分,一眼先认出「哪一类」,图标认具体类型 */
--nt-input: #b1ceee;
--nt-output: #93afd4;
--nt-llm: #c7c2ff;
--nt-agent: #b49af8;
--nt-supervisor: #bcadff;
--nt-tool: #10c4c9;
--nt-code: #57e3df;
--nt-merge: #96efe6;
--nt-branch: #e389ca;
--nt-loop: #ffaddd;
--nt-subgraph: #dba9c8;
--nt-memory: #1ebeeb;
--nt-retrieve: #84d7fe;
--nt-transform: #93c3d8;
--nt-human: #ce91e6;
--nt-validate: #ebb1fd;
--nt-metrics: #cbaddb;
--nt-analysis: #c09dd9;
--nt-report: #a9b0ec;
--nt-dual_check: #a98ee0;
--nt-challenge: #dcb7ef;
/* 检索通路的分类色:只说「这一分是哪一路检索贡献的」,不表示状态,所以
不借 accent / ok / warn。两色按 dataviz 校验过:色觉异常下也分得开,
在 panel 底上作为图形 ≥ 3:1 */
--via-semantic: #2b95c0;
--via-keyword: #d55181;
/* 应用页图表(趋势、图表)的系列色,按固定顺序取,不轮换。dataviz 的参考色板按面板底色(#141926 / #ffffff)
跑过校验:深色下四色对底色都 ≥ 3:1、相邻两色色觉异常下也分得开;浅色下第 3、4 色对底色不足 3:1,
图表一律带「查看数据」的表格视图和图例,不靠颜色单独认 */
--viz-1: #3987e5;
--viz-2: #d95926;
--viz-3: #199e70;
--viz-4: #c98500;
}
:root[data-theme='light'] {
color-scheme: light;
--bg: #f6f7f9;
--bg-elev: #ffffff;
--bg-panel: #ffffff;
--bg-hover: #eef1f6;
--border: #dfe3ea;
--border-strong: #c3cad6;
--text: #1a1f2b;
--text-dim: #4a5467;
--text-faint: #636d80;
--accent: #2563eb;
--accent-soft: #e8f0fe;
--accent-solid: #2563eb;
--on-accent: #ffffff;
--ok: #187a34;
--warn: #946300;
--warn-solid: #946300;
--on-warn: #ffffff;
--err: #cf222e;
--err-solid: #cf222e;
--on-err: #ffffff;
--copilot: #8250df;
--grid-dot: #c9cfda;
--grid-dot-major: #b4bcc9;
--hairline: #e8ebf0;
--elev-1: 0 1px 2px rgb(16 24 40 / 0.06);
--elev-2: 0 6px 16px -4px rgb(16 24 40 / 0.12), 0 2px 4px -2px rgb(16 24 40 / 0.06);
--elev-3: 0 20px 48px -12px rgb(16 24 40 / 0.22), 0 8px 16px -8px rgb(16 24 40 / 0.1);
--st-queued: #4468b5;
--st-suspended: #6a5c3e;
--st-blocked: #994857;
--st-unreached: #80899a;
--nt-input: #546e8b;
--nt-output: #415774;
--nt-llm: #695ead;
--nt-agent: #5c448f;
--nt-supervisor: #63519e;
--nt-tool: #026264;
--nt-code: #057c79;
--nt-merge: #0a4d5a;
--nt-branch: #7e376c;
--nt-loop: #9b4c7d;
--nt-subgraph: #7a516c;
--nt-memory: #025f78;
--nt-retrieve: #00769d;
--nt-transform: #3e6778;
--nt-human: #6f3d81;
--nt-validate: #885398;
--nt-metrics: #6e557b;
--nt-analysis: #674a82;
--nt-report: #4f5694;
--nt-dual_check: #5a4a91;
--nt-challenge: #7d4c8f;
--via-semantic: #0a7ea4;
--via-keyword: #c2447a;
--viz-1: #2a78d6;
--viz-2: #eb6834;
--viz-3: #1baf7a;
--viz-4: #eda100;
}
/* 「跟随系统」且系统是浅色:和上面那块逐字相同 */
@media (prefers-color-scheme: light) {
:root:not([data-theme]) {
color-scheme: light;
--bg: #f6f7f9;
--bg-elev: #ffffff;
--bg-panel: #ffffff;
--bg-hover: #eef1f6;
--border: #dfe3ea;
--border-strong: #c3cad6;
--text: #1a1f2b;
--text-dim: #4a5467;
--text-faint: #636d80;
--accent: #2563eb;
--accent-soft: #e8f0fe;
--accent-solid: #2563eb;
--on-accent: #ffffff;
--ok: #187a34;
--warn: #946300;
--warn-solid: #946300;
--on-warn: #ffffff;
--err: #cf222e;
--err-solid: #cf222e;
--on-err: #ffffff;
--copilot: #8250df;
--grid-dot: #c9cfda;
--grid-dot-major: #b4bcc9;
--hairline: #e8ebf0;
--elev-1: 0 1px 2px rgb(16 24 40 / 0.06);
--elev-2: 0 6px 16px -4px rgb(16 24 40 / 0.12), 0 2px 4px -2px rgb(16 24 40 / 0.06);
--elev-3: 0 20px 48px -12px rgb(16 24 40 / 0.22), 0 8px 16px -8px rgb(16 24 40 / 0.1);
--st-queued: #4468b5;
--st-suspended: #6a5c3e;
--st-blocked: #994857;
--st-unreached: #80899a;
--nt-input: #546e8b;
--nt-output: #415774;
--nt-llm: #695ead;
--nt-agent: #5c448f;
--nt-supervisor: #63519e;
--nt-tool: #026264;
--nt-code: #057c79;
--nt-merge: #0a4d5a;
--nt-branch: #7e376c;
--nt-loop: #9b4c7d;
--nt-subgraph: #7a516c;
--nt-memory: #025f78;
--nt-retrieve: #00769d;
--nt-transform: #3e6778;
--nt-human: #6f3d81;
--nt-validate: #885398;
--nt-metrics: #6e557b;
--nt-analysis: #674a82;
--nt-report: #4f5694;
--nt-dual_check: #5a4a91;
--nt-challenge: #7d4c8f;
--via-semantic: #0a7ea4;
--via-keyword: #c2447a;
--viz-1: #2a78d6;
--viz-2: #eb6834;
--viz-3: #1baf7a;
--viz-4: #eda100;
}
}
/* 由上面推出来的令牌,两套主题共用,只写一次 */
:root {
/* 状态语义。组件里只写 --st-*,不直接写 ok / warn / err / accent——
哪天「运行中」要换颜色,改这一处就够了。-soft 是同色的浅底 */
--st-idle: var(--text-faint);
--st-running: var(--accent);
--st-done: var(--ok);
--st-waiting: var(--warn);
--st-failed: var(--err);
--st-skipped: var(--text-faint);
--st-cancelled: var(--text-dim);
--st-idle-soft: color-mix(in srgb, var(--st-idle) 10%, transparent);
--st-queued-soft: color-mix(in srgb, var(--st-queued) 14%, transparent);
--st-running-soft: color-mix(in srgb, var(--st-running) 14%, transparent);
--st-waiting-soft: color-mix(in srgb, var(--st-waiting) 14%, transparent);
--st-done-soft: color-mix(in srgb, var(--st-done) 14%, transparent);
--st-failed-soft: color-mix(in srgb, var(--st-failed) 14%, transparent);
--st-skipped-soft: color-mix(in srgb, var(--st-skipped) 14%, transparent);
--st-cancelled-soft: color-mix(in srgb, var(--st-cancelled) 14%, transparent);
--st-suspended-soft: color-mix(in srgb, var(--st-suspended) 14%, transparent);
--st-blocked-soft: color-mix(in srgb, var(--st-blocked) 14%, transparent);
--st-unreached-soft: color-mix(in srgb, var(--st-unreached) 10%, transparent);
/* 「有东西在跑」的颜色。和 --st-running 是一回事,给外壳、品牌标这类
不属于某个节点的地方用 */
--live: var(--st-running);
/* 当前焦点的光晕。只给一个元素用(正在看的那个节点、聚焦的输入坞),
满屏都在发光就等于没有焦点 */
--glow-accent: 0 0 0 1px color-mix(in srgb, var(--accent) 70%, transparent),
0 0 14px -2px color-mix(in srgb, var(--accent) 45%, transparent);
/* 动效。时长三档:fast 给 hover / 按下,base 给面板开合,slow 给一次性的
入场和「时刻」。缓动 out 是「起步快、尾部长」——被推出来的手感 */
--dur-fast: 120ms;
--dur-base: 200ms;
--dur-slow: 360ms;
}
/* 字号阶梯。中文正文不低于 11px(2xs);画布卡片内部和 360px 的助手栏暂时
不受这条约束,它们的可读性靠按缩放分级渲染解决。
static:这些变量不管 Tailwind 扫没扫到都输出,单独的 .css 文件里也能用 var() */
@theme static {
--text-2xs: 11px;
--text-2xs--line-height: calc(16 / 11);
--text-xs: 12px;
--text-xs--line-height: calc(16 / 12);
--text-sm: 13px;
--text-sm--line-height: calc(20 / 13);
--text-base: 14px;
--text-base--line-height: calc(22 / 14);
--text-lg: 16px;
--text-lg--line-height: calc(24 / 16);
--text-xl: 20px;
--text-xl--line-height: calc(28 / 20);
--text-2xl: 24px;
--text-2xl--line-height: calc(32 / 24);
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--ease-out: cubic-bezier(0.32, 0.72, 0, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
--font-mono: 'SF Mono', ui-monospace, 'JetBrains Mono', Menlo, Consolas, monospace;
}
@theme inline {
--color-bg: var(--bg);
--color-elev: var(--bg-elev);
--color-panel: var(--bg-panel);
--color-hover: var(--bg-hover);
--color-line: var(--border);
--color-line-strong: var(--border-strong);
--color-hairline: var(--hairline);
--color-fg: var(--text);
--color-dim: var(--text-dim);
--color-faint: var(--text-faint);
--color-accent: var(--accent);
--color-accent-soft: var(--accent-soft);
--color-accent-solid: var(--accent-solid);
--color-on-accent: var(--on-accent);
--color-ok: var(--ok);
--color-warn: var(--warn);
--color-warn-solid: var(--warn-solid);
--color-on-warn: var(--on-warn);
--color-err: var(--err);
--color-err-solid: var(--err-solid);
--color-on-err: var(--on-err);
--color-copilot: var(--copilot);
--color-st-idle: var(--st-idle);
--color-st-queued: var(--st-queued);
--color-st-running: var(--st-running);
--color-st-waiting: var(--st-waiting);
--color-st-done: var(--st-done);
--color-st-failed: var(--st-failed);
--color-st-skipped: var(--st-skipped);
--color-st-cancelled: var(--st-cancelled);
--color-st-suspended: var(--st-suspended);
--color-st-blocked: var(--st-blocked);
--color-st-unreached: var(--st-unreached);
--shadow-elev-1: var(--elev-1);
--shadow-elev-2: var(--elev-2);
--shadow-elev-3: var(--elev-3);
}
/* 等宽数字:计时器、耗时、token 数刷新时宽度不跳,列与列对得齐 */
@utility tnum {
font-variant-numeric: tabular-nums;
}
/* 全局规则放进 base 层。以前它们不在任何层里,而不分层的样式无论优先级
多低都压过分层的——Tailwind 的工具类全在 utilities 层,于是写在元素上的
border-[var(--accent)]、border-transparent 统统失效,发布弹窗选了「受管」
也看不出来 */
@layer base {
*,
::before,
::after,
::backdrop,
::file-selector-button {
border-color: var(--border);
}
:root {
/* 原生复选框、单选、滑块跟着强调色走;深浅由上面的 color-scheme 管 */
accent-color: var(--accent);
}
/* 切主题那一帧关掉过渡,见 lib/theme.ts */
:root.theme-switching *,
:root.theme-switching *::before,
:root.theme-switching *::after {
transition: none !important;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: 13px;
-webkit-font-smoothing: antialiased;
}
code,
pre,
.mono {
font-family: var(--font-mono);
}
/* 滚动条:细一点,不抢视觉。别加 scrollbar-color——写了它 Chrome 就不认
::-webkit-scrollbar 了 */
::-webkit-scrollbar {
width: 9px;
height: 9px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-strong);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-faint);
}
}
/* --------------------------------------------------------------------------
节点类型配色。色值在上面的 --nt-* 里(两套主题各一份);这里只把它接到
--nt 上,同一个色相贯穿图标底色、标题栏、协作矩阵。小地图等别处要类型色,
直接读 var(--nt-<type>),不要再抄一份 hex。
-------------------------------------------------------------------------- */
.nt-input { --nt: var(--nt-input); }
.nt-output { --nt: var(--nt-output); }
.nt-llm { --nt: var(--nt-llm); }
.nt-agent { --nt: var(--nt-agent); }
.nt-supervisor { --nt: var(--nt-supervisor); }
.nt-tool { --nt: var(--nt-tool); }
.nt-code { --nt: var(--nt-code); }
.nt-merge { --nt: var(--nt-merge); }
.nt-branch { --nt: var(--nt-branch); }
.nt-loop { --nt: var(--nt-loop); }
.nt-human { --nt: var(--nt-human); }
.nt-validate { --nt: var(--nt-validate); }
.nt-memory { --nt: var(--nt-memory); }
.nt-retrieve { --nt: var(--nt-retrieve); }
.nt-transform { --nt: var(--nt-transform); }
.nt-subgraph { --nt: var(--nt-subgraph); }
.nt-metrics { --nt: var(--nt-metrics); }
.nt-analysis { --nt: var(--nt-analysis); }
.nt-report { --nt: var(--nt-report); }
.nt-dual_check { --nt: var(--nt-dual_check); }
.nt-challenge { --nt: var(--nt-challenge); }
/* --------------------------------------------------------------------------
React Flow 覆盖
-------------------------------------------------------------------------- */
.react-flow {
background: var(--bg);
}
.react-flow__attribution {
display: none;
}
.react-flow__handle {
width: 9px;
height: 9px;
background: var(--bg-elev);
border: 2px solid var(--border-strong);
transition-property: border-color, background-color, box-shadow, transform;
transition-duration: var(--dur-fast);
}
.react-flow__handle:hover {
border-color: var(--accent);
transform: scale(1.25);
}
.react-flow__handle-connecting {
border-color: var(--accent);
}
.react-flow__edge-path {
stroke: var(--border-strong);
stroke-width: 1.6;
}
.react-flow__edge.selected .react-flow__edge-path {
stroke: var(--accent);
stroke-width: 2.4;
}
.react-flow__edge-text {
fill: var(--text-dim);
font-size: 10px;
}
.react-flow__controls-button {
background: var(--bg-panel);
border-color: var(--border);
fill: var(--text-dim);
}
.react-flow__controls-button:hover {
background: var(--bg-hover);
}
.react-flow__minimap {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 6px;
}
/* --------------------------------------------------------------------------
节点卡。
状态四重编码:剪影(StatusBadge)+ 线型 / 左侧状态槽的纹理 + 状态色 + 文字。
去掉任何一条都还认得出来——灰度投影、色弱、关了动效、缩到 0.3 倍都一样。
颜色只给状态用:边框、光弧、状态槽一律 --st-*;类型色 --nt 只留在图标块和
标题栏极淡的底色里(以前运行中的分支节点是琥珀框,和「等人审批」分不开)。
正常态安静、异常态醒目:只有等待、失败、挂起才铺底色、加粗边(用 0.5px 的
box-shadow 加粗,不改 border-width——边框一变宽盒子就变,走线跟着重算)。
box-shadow 分四层拼起来,各层只写自己的变量,谁也不盖谁:选中圈 --nc-ring、
悬停联动 --nc-focus、Copilot 新节点 --nc-copilot、状态的分量 --nc-weight。
以前各写各的 box-shadow,悬停一张等待中的卡,琥珀的加粗和 elev-2 就没了;
Copilot 刚放上来的节点点选中,选中圈也被紫色盖掉。
卡片高度在整个运行期间不变:标题 32 + 正文两行 36 + 遥测槽 22。流式输出原位
替换摘要那两行,计时、token、工具、循环进度都收进遥测槽。以前输出框把卡片
撑高 80px,正好压住下面那个节点。
-------------------------------------------------------------------------- */
.nc {
position: relative;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--bg-elev);
box-shadow: var(--nc-ring, 0 0 #0000), var(--nc-focus, 0 0 #0000),
var(--nc-copilot, 0 0 #0000), var(--nc-weight, var(--elev-1));
/* 状态的压暗(未到达、阻断)和悬停联动的压暗各管各的,乘起来 */
opacity: calc(var(--nc-op, 1) * var(--nc-dim, 1));
transition: opacity var(--dur-base) var(--ease-out);
}
.nc-head {
display: flex;
align-items: center;
gap: 7px;
height: 32px;
padding: 0 9px 0 10px;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
background: color-mix(in srgb, var(--nt) 6%, transparent);
}
.nc-icon {
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--nt) 18%, transparent);
color: var(--nt);
}
.nc-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
}
.nc-chip {
flex: none;
height: 15px;
padding: 0 4px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-dim);
font-size: 9.5px;
line-height: 13px;
white-space: nowrap;
}
.nc-chip-warn {
border-color: color-mix(in srgb, var(--st-waiting) 45%, transparent);
color: var(--st-waiting);
}
.nc-badge {
flex: none;
}
/* 正文:固定两行。摘要和流式输出轮流占这个位置,谁也不把卡片撑高 */
.nc-body {
height: 36px;
padding: 4px 10px 0;
overflow: hidden;
color: var(--text-dim);
font-size: 11px;
line-height: 14px;
}
.nc-summary {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
overflow-wrap: anywhere;
}
/* 流式尾巴:底对齐,新出的字总在最下面那行,旧的从顶上裁掉 */
.nc-stream {
display: flex;
flex-direction: column;
justify-content: flex-end;
height: 28px;
overflow: hidden;
font-size: 10px;
line-height: 14px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.nc-stream-tag {
color: var(--text-faint);
font-family: var(--font-sans);
}
/* 遥测槽 */
.nc-tele {
display: flex;
align-items: center;
gap: 6px;
height: 22px;
padding: 0 10px;
overflow: hidden;
border-top: 1px solid var(--hairline);
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
color: var(--text-faint);
font-size: 10px;
white-space: nowrap;
}
.nc-tele-state {
flex: none;
font-weight: 600;
}
.nc-tele-main {
display: inline-flex;
flex: none;
align-items: center;
gap: 5px;
color: var(--text-dim);
font-family: var(--font-mono);
}
.nc-tele-note {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
/* 跑完了但有要留意的事:降档交付、收尾时还想调工具 */
.nc-tele-note.is-warn {
color: var(--st-waiting);
}
/* 工具调用超出了后端声明的时限:只有这时才上色,平常是安静的「⋯」 */
.nc-over {
color: var(--st-waiting);
font-family: var(--font-sans);
font-weight: 600;
}
.nc-tele-side {
display: flex;
flex: 0 1 auto;
min-width: 0;
align-items: center;
gap: 7px;
font-family: var(--font-mono);
}
.nc-tele-side > * {
flex: none;
}
/* 在跑的工具名可以很长(db_query__warehouse):只有它让位,截成省略号 */
.nc-tele-side > .nc-tools {
flex: 0 1 auto;
min-width: 0;
}
.nc-tools {
display: inline-flex;
align-items: center;
gap: 2px;
white-space: nowrap;
}
.nc-tools > svg {
flex: none;
}
.nc-tool-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.nc-tools.is-live {
color: var(--st-running);
}
.nc-tools.is-bad {
color: var(--st-failed);
}
.nc-tele-dash {
color: var(--text-faint);
}
.nc-tele-issue {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.nc-tele-issue.is-error {
color: var(--st-failed);
}
.nc-tele-issue.is-warning {
color: var(--st-waiting);
}
/* 「去审批」:画出来的药丸只有 16px 高(0.65 倍时屏幕上 10px),按钮本身撑满
遥测槽的高度、再往右吃进槽的内边距,点击区比看到的大一圈。药丸画在 ::before 上。
不用往外扩的伪元素:遥测槽是 overflow:hidden,扩出去的部分点不到 */
.nc-approve {
position: relative;
z-index: 0;
flex: none;
align-self: stretch;
margin-right: -6px;
padding: 0 13px 0 7px;
color: var(--on-warn);
font-size: 10px;
font-weight: 600;
line-height: 16px;
cursor: pointer;
}
.nc-approve::before {
content: '';
position: absolute;
inset: 2.5px 6px 2.5px 0;
z-index: -1;
border-radius: var(--radius-sm);
background: var(--warn-solid);
}
.nc-approve:hover::before {
background: color-mix(in srgb, var(--warn-solid) 86%, #000);
}
.nc-approve:focus-visible {
outline: none;
}
.nc-approve:focus-visible::before {
outline: 2px solid var(--accent);
}
/* 循环的分段进度:一格一项(或一轮),做完的绿、在做的蓝 */
.nc-seg {
display: inline-flex;
flex: none;
gap: 2px;
}
.nc-seg i {
width: 9px;
height: 5px;
border-radius: 1px;
background: var(--bg-hover);
box-shadow: inset 0 0 0 0.5px var(--border-strong);
}
.nc-seg i.is-done {
background: color-mix(in srgb, var(--st-done) 70%, transparent);
box-shadow: none;
}
.nc-seg i.is-cur {
background: var(--st-running);
box-shadow: none;
}
.nc-seg-bar {
position: relative;
width: 64px;
height: 5px;
overflow: hidden;
border-radius: 1px;
background: var(--bg-hover);
}
.nc-seg-bar i {
position: absolute;
inset: 0 auto 0 0;
height: auto;
border-radius: 0;
background: color-mix(in srgb, var(--st-done) 70%, transparent);
box-shadow: none;
}
/* 左侧状态槽:纹理本身就是编码——实心 = 在跑,断续 = 排队,斜纹 = 等人,
粗实心 = 失败,两道竖条 = 挂起 */
.nc-slot {
position: absolute;
top: -1px;
bottom: -1px;
left: -1px;
z-index: 2;
width: 3px;
border-radius: var(--radius-lg) 0 0 var(--radius-lg);
pointer-events: none;
}
/* ---- 各状态 ---- */
.nc.node-queued {
--st-c: var(--st-queued);
border-style: dashed;
border-color: color-mix(in srgb, var(--st-queued) 65%, var(--border));
}
.node-queued .nc-slot {
background: repeating-linear-gradient(to bottom, var(--st-queued) 0 4px, transparent 4px 7px);
}
.nc.node-running {
--st-c: var(--st-running);
border-color: var(--st-running);
}
.node-running .nc-slot {
background: var(--st-running);
}
.nc.node-waiting {
--st-c: var(--st-waiting);
border-color: var(--st-waiting);
--nc-weight: 0 0 0 0.5px var(--st-waiting), var(--elev-2);
}
.node-waiting .nc-slot {
width: 5px;
background: repeating-linear-gradient(135deg, var(--st-waiting) 0 3px, transparent 3px 6px);
}
.node-waiting .nc-tele {
background: var(--st-waiting-soft);
}
.nc.node-done {
--st-c: var(--st-done);
border-color: color-mix(in srgb, var(--st-done) 45%, var(--border));
}
.node-done .nc-slot {
width: 2px;
background: color-mix(in srgb, var(--st-done) 75%, transparent);
}
.nc.node-failed {
--st-c: var(--st-failed);
border-color: var(--st-failed);
--nc-weight: 0 0 0 0.5px var(--st-failed), var(--elev-2);
}
.node-failed .nc-slot {
width: 5px;
background: var(--st-failed);
}
.node-failed .nc-tele {
background: var(--st-failed-soft);
}
.node-failed .nc-tele-note {
color: var(--st-failed);
}
.nc.node-cancelled {
--st-c: var(--st-cancelled);
--nc-op: 0.8;
}
.node-cancelled .nc-slot {
background: var(--st-cancelled);
}
/* 被人驳回的那一步(node.failed 带 reason=rejected):人的决定,不是故障。和取消同一种中性色,不画失败的红框、不抖动 */
.nc.node-rejected {
--st-c: var(--st-cancelled);
border-color: color-mix(in srgb, var(--st-cancelled) 70%, var(--border));
}
.node-rejected .nc-slot {
width: 5px;
background: var(--st-cancelled);
}
.nc.node-suspended {
--st-c: var(--st-suspended);
border-color: color-mix(in srgb, var(--st-suspended) 70%, var(--border));
}
.node-suspended .nc-slot {
width: 5px;
background: repeating-linear-gradient(to right, var(--st-suspended) 0 1.5px, transparent 1.5px 3.5px);
}
.node-suspended .nc-tele {
background: var(--st-suspended-soft);
}
.nc.node-skipped {
--st-c: var(--st-skipped);
border-style: dashed;
border-color: var(--border-strong);
}
.nc.node-blocked {
--st-c: var(--st-blocked);
--nc-op: 0.62;
border-style: dotted;
border-color: color-mix(in srgb, var(--st-blocked) 75%, transparent);
}
.node-blocked .nc-tele-note {
color: var(--st-blocked);
}
.nc.node-unreached {
--st-c: var(--st-unreached);
--nc-op: 0.45;
border-style: dashed;
}
/* 选中:外面隔一圈底色再描一圈强调色。运行中也是强调色的边框,靠这圈空隙分开,
不然「选中了」和「在跑」长得一样 */
.nc.nc-selected {
--nc-ring: 0 0 0 2px var(--bg), 0 0 0 3.5px var(--accent),
0 0 16px -2px color-mix(in srgb, var(--accent) 45%, transparent);
}
/* 右栏悬停某一步:对应的节点亮一圈,其余的退下去 */
.nc.nc-hover {
--nc-focus: 0 0 0 4px color-mix(in srgb, var(--accent) 16%, transparent), var(--glow-accent);
}
.nc.nc-dim {
--nc-dim: 0.55;
}
/* Copilot 刚放上来的节点:紫色描边,落位时从上方轻轻落下一次。
只在节点还没有运行态时画——运行中、失败、等待的边框永远压过编辑态的紫色,
否则停掉一次生成接着试跑,挂掉的那个节点是紫框,一眼看不出谁挂了 */
@keyframes copilot-land {
from { opacity: 0; transform: translateY(-8px) scale(0.96); }
}
.nc.node-copilot-new {
animation: copilot-land var(--dur-slow) var(--ease-out) 1 both;
}
.nc.node-copilot-new[data-state='idle'] {
border-color: var(--copilot);
--nc-copilot: 0 0 0 3px color-mix(in srgb, var(--copilot) 24%, transparent);
}
/* 端口不可连时收起来,不给「还能连」的暗示。可不可连只看 React Flow 给端口的
connectable 类(运行进行中画布关掉 nodesConnectable,正式、探索都一样),
不在这里按运行类别再推一遍。跑中已经亮起的出口照样亮着 */
.nc .react-flow__handle:not(.connectable) {
cursor: default;
pointer-events: none;
}
.nc .react-flow__handle:not(.connectable):not(.handle-taken) {
opacity: 0.3;
}
/* 正式运行进行中:跑的是发布快照,画布只读,拖也拖不动 */
.react-flow:is([data-readonly], [data-run-class='formal']:is([data-run-phase='queued'], [data-run-phase='running'], [data-run-phase='waiting'])) .nc {
cursor: default;
}
/* 出口标签:画在出线第一段的上方(标签底边离线 2px),不压线。底色和画布同色,
别的线从后面穿过时字照样读得出。运行前一律中性色,命中的那条亮起来 */
.nc-exit {
position: absolute;
left: calc(100% + 8px);
z-index: 1;
padding: 0 3px;
transform: translateY(calc(-100% - 2px));
border-radius: var(--radius-xs);
background: var(--bg);
color: var(--text-faint);
font-size: 9.5px;
line-height: 13px;
white-space: nowrap;
pointer-events: none;
}
.nc-exit.is-hit {
color: var(--st-running);
font-weight: 600;
}
.nc-exit.is-miss {
opacity: 0.45;
}
/* 出口密(5 个起间距不到 16px):画在线上方的标签会压住上一条出口的线,
改成骑在自己那条线上——标签只截断自己的线,谁是谁的不会看错 */
.nc-exits-dense .nc-exit {
transform: translateY(-50%);
font-size: 9px;
line-height: 11px;
}
/* 出口被选中/落空:分支节点跑完要能一眼看出走了哪条 */
.react-flow__handle.handle-taken {
background: var(--st-running);
border-color: var(--st-running);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--st-running) 30%, transparent);
}
.react-flow__handle.handle-idle {
opacity: 0.35;
}
/* 校验问题的角标。error 和 warning 一样醒目——warning 恰恰是"运行时静默取到
空值"那一类,以前有 error 就看不见它,卡片上也根本不画 */
.nc-issue {
position: absolute;
top: -8px;
right: -8px;
z-index: 3;
display: flex;
gap: 2px;
transform-origin: top right;
}
.nc-issue span {
height: 16px;
padding: 0 5px;
border-radius: 8px;
box-shadow: 0 0 0 2px var(--bg);
font-size: 10px;
font-weight: 600;
line-height: 16px;
}
.nc-issue .is-error {
background: var(--err-solid);
color: var(--on-err);
}
.nc-issue .is-warning {
background: var(--warn-solid);
color: var(--on-warn);
}
/* 出具印章:双线框,成果节点右上角。档位用状态色:完整出具 = 完成的绿,
降档 = 琥珀,不予出具 = 红 */
.nc-stamp {
position: absolute;
top: -12px;
right: 22px;
z-index: 3;
display: flex;
align-items: center;
gap: 4px;
height: 21px;
padding: 0 7px;
border: 3px double currentColor;
border-radius: var(--radius-sm);
background: var(--bg-elev);
box-shadow: 0 0 0 2px var(--bg-elev), var(--elev-1);
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.06em;
white-space: nowrap;
}
.nc-stamp.is-link {
cursor: pointer;
}
.nc-stamp small {
color: var(--text-faint);
font-size: 9px;
font-weight: 500;
letter-spacing: 0;
}
/* 报告节点的章(引用 N · 无证据 M)放在卡片下沿:上沿左边是血缘标签(产出 / 引用),右边是校验角标 */
.nc-stamp.is-evidence {
top: auto;
/* 只压住卡片下沿 4px:卡片最后一行(校验提示、耗时)不被章挡住 */
bottom: -17px;
right: 14px;
letter-spacing: 0;
}
.nc-stamp.is-formal { color: var(--st-done); }
.nc-stamp.is-degraded { color: var(--st-waiting); }
.nc-stamp.is-withheld { color: var(--st-failed); }
@keyframes nc-stamp {
from { opacity: 0; transform: scale(1.4) rotate(-5deg); }
60% { opacity: 1; }
to { opacity: 1; transform: none; }
}
/* 落章只在实时的出具判定到来时播一次,300ms 以内 */
.nc-stamp-in {
animation: nc-stamp 280ms var(--ease-out) 1 both;
}
/* 「去审批」浮层:挂在 body 上,屏幕空间,不随画布缩放 */
.nc-pop {
position: fixed;
z-index: 60;
width: 340px;
max-height: min(70vh, 520px);
overflow: auto;
border: 1px solid var(--st-waiting);
border-radius: var(--radius-lg);
background: var(--bg-panel);
box-shadow: var(--elev-3);
}
/* 精简档和信号档的替身层。平时不可见,但一直在:按档切换只改 visibility,
不改盒子——display:none 会让卡片高度变,React Flow 重新量尺寸、走线全部重算 */
.nc-lod,
.nc-sig {
position: absolute;
inset: 0;
z-index: 2;
visibility: hidden;
pointer-events: none;
}
.nc-lod {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
padding: 0 14px 0 16px;
}
/* 字号按缩放反算:精简档一路到 0.34 倍,屏幕上都是标题 11.5px、读数 9.5px;
回滞让这一档最低到 0.33,那时封顶生效,仍有 11.2 / 9.2px。封顶是为了不撑出
卡片:34 × 1.15 + 28 × 1.2 + 4 ≈ 77px,装得进 92px 的盒子 */
.nc-lod-title {
display: flex;
align-items: center;
gap: 0.35em;
min-width: 0;
font-size: min(34px, calc(11.5px / var(--zoom, 1)));
font-weight: 600;
line-height: 1.15;
}
.nc-lod-title svg {
width: 0.9em;
height: 0.9em;
}
.nc-lod-title span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nc-lod-read {
overflow: hidden;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: min(28px, calc(9.5px / var(--zoom, 1)));
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.nc-lod-read .nc-tenth,
.nc-lod-read .nc-seg {
display: none;
}
.nc-sig {
display: flex;
align-items: center;
justify-content: center;
}
.nc-sig svg {
/* 反向缩放:0.3 倍时屏幕上约 16px,封顶免得比卡片还高 */
width: min(56px, calc(16px / var(--zoom, 1)));
height: min(56px, calc(16px / var(--zoom, 1)));
}
.react-flow[data-lod='compact'] .nc-content,
.react-flow[data-lod='signal'] .nc-content,
.react-flow[data-lod='compact'] .nc-exit,
.react-flow[data-lod='signal'] .nc-exit {
visibility: hidden;
}
.react-flow[data-lod='compact'] .nc-lod,
.react-flow[data-lod='signal'] .nc-sig {
visibility: visible;
}
/* 远了扫描线只剩一片模糊的亮斑,而且一张大图里每张都在扫,关掉 */
.react-flow[data-lod='compact'] .fx-scan,
.react-flow[data-lod='signal'] .fx-scan {
display: none;
}
.react-flow[data-lod='compact'] .nc-slot {
width: 6px;
}
.react-flow[data-lod='compact'] :is(.node-waiting, .node-failed, .node-suspended) .nc-slot {
width: 10px;
}
.react-flow[data-lod='compact'] .nc-issue,
.react-flow[data-lod='signal'] .nc-issue {
transform: scale(calc(0.9 / var(--zoom, 1)));
}
/* 印章同样反向缩放,远景里也读得出档位。用独立的 scale 属性:落章动画占着
transform,写在 transform 上会被动画的末帧盖掉 */
.react-flow[data-lod='compact'] .nc-stamp,
.react-flow[data-lod='signal'] .nc-stamp {
transform-origin: bottom right;
scale: calc(0.9 / var(--zoom, 1));
}
/* 信号档:整张卡就是一块状态色。颜色之外还有纹理(等人斜纹、挂起竖条)、
线型(排队 / 未到达虚线、阻断点线)和居中的剪影 */
.react-flow[data-lod='signal'] .nc {
background: color-mix(in srgb, var(--st-c, transparent) 16%, var(--bg-elev));
--nc-weight: inset 0 0 0 calc(1px / var(--zoom, 1)) color-mix(in srgb, var(--st-c, var(--border-strong)) 70%, transparent);
}
.react-flow[data-lod='signal'] .nc .nc-slot {
display: none;
}
.react-flow[data-lod='signal'] .nc.node-running {
background: color-mix(in srgb, var(--st-running) 34%, var(--bg-elev));
}
.react-flow[data-lod='signal'] .nc.node-failed {
background: color-mix(in srgb, var(--st-failed) 78%, var(--bg-elev));
--nc-weight: 0 0 0 calc(1.5px / var(--zoom, 1)) var(--st-failed);
}
.react-flow[data-lod='signal'] .nc.node-waiting {
background: repeating-linear-gradient(135deg,
color-mix(in srgb, var(--st-waiting) 55%, var(--bg-elev)) 0 14px, var(--bg-elev) 14px 28px);
--nc-weight: 0 0 0 calc(1.5px / var(--zoom, 1)) var(--st-waiting);
}
.react-flow[data-lod='signal'] .nc.node-suspended {
background: repeating-linear-gradient(90deg,
color-mix(in srgb, var(--st-suspended) 45%, var(--bg-elev)) 0 12px, var(--bg-elev) 12px 30px);
}
/* 失败那块是实心红,剪影反白才看得见(徽标自带行内颜色,这里要压过它) */
.react-flow[data-lod='signal'] .nc.node-failed .nc-sig svg {
color: var(--on-err) !important;
}
/* 牌子挂在卡片外面,会被后画的邻居节点盖住:异常节点整体抬到最上层 */
.react-flow[data-lod='signal'] .react-flow__node:has(> .nc:is(.node-failed, .node-waiting)) {
z-index: 1000 !important;
}
/* 信号档的牌子:失败和等人的节点头上挂一枚,字号反向缩放,任何缩放下都是 11px */
.nc-pin {
position: absolute;
bottom: 100%;
left: 0;
z-index: 5;
display: flex;
align-items: center;
gap: 0.4em;
margin-bottom: 0.5em;
padding: 0.3em 0.65em;
border: 0.09em solid var(--st-c);
border-radius: 0.45em;
background: var(--bg-panel);
box-shadow: var(--elev-2);
color: var(--text-dim);
font-size: calc(11px / var(--zoom, 1));
line-height: 1.3;
white-space: nowrap;
visibility: hidden;
pointer-events: none;
}
.nc-pin svg {
width: 1.1em;
height: 1.1em;
}
.nc-pin b {
color: var(--st-c);
font-weight: 600;
}
.nc-pin em {
color: var(--st-c);
font-family: var(--font-mono);
font-style: normal;
}
.react-flow[data-lod='signal'] .nc-pin {
visibility: visible;
}
/* --------------------------------------------------------------------------
运行时的光效语汇。
一条原则:**每个动效都要对应一件真实发生的事**。光弧转 = 这个节点正在跑,
扫描线过 = 正在产出,绿色扫过 = 刚跑完,抖动 = 刚失败,琥珀一圈 = 刚停下来
等人。纯装饰的动效只会让画布变吵——跑一张十几个节点的图,一眼能看出
"谁在动"才是这套东西的用处。
常驻的只有运行中那一圈光弧和扫描线(外加徽标的转弧)。等待、完成和各种终态
都是静态的:以前等人时琥珀色一直在呼吸,停在审批上半小时,画布就半小时在动。
进入某个状态的那一下(抖、扫、琥珀一圈)只在实时事件里播,回放和打开历史
运行时直接落到终态(NodeCard 给这一下挂 .nc-enter)。
颜色一律走 --st-* 令牌,浅色深色都能看。前庭敏感的人由文件末尾那段
prefers-reduced-motion 统一停掉。
-------------------------------------------------------------------------- */
/* 光环的角度要能插值,得先声明成一个 <angle> 属性 */
@property --fx-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
/* 光效层。做成独立一层而不是伪元素:卡片自己的背景会盖住伪元素,
而 overflow:hidden 又不能加在卡片上——出口标签、印章、牌子都挂在卡片外面,
一裁就没了 */
.node-fx {
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
pointer-events: none;
}
/* 只负责裁剪的那一层:正好是卡片框,扫描线不会漏到邻居卡片上 */
.fx-clip {
position: absolute;
inset: 0;
overflow: hidden;
border-radius: inherit;
}
/* 正在跑:一圈绕着卡片转的光弧。状态色,不是类型色 */
@keyframes fx-halo {
to { --fx-angle: 360deg; }
}
.fx-halo {
position: absolute;
inset: -1.5px;
padding: 1.5px;
border-radius: inherit;
background: conic-gradient(
from var(--fx-angle),
transparent 0deg,
transparent 300deg,
color-mix(in srgb, var(--st-running) 65%, transparent) 340deg,
var(--st-running) 358deg,
transparent 360deg
);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
animation: fx-halo 2.2s linear infinite;
}
/* 正在跑:一条光带从上往下扫过卡片,像在读取/产出 */
@keyframes fx-scan {
0% { transform: translateY(-70px); opacity: 0; }
15% { opacity: 1; }
85% { opacity: 1; }
100% { transform: translateY(var(--scan-h, 120px)); opacity: 0; }
}
.fx-scan {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 48px;
background: linear-gradient(
to bottom,
transparent,
color-mix(in srgb, var(--st-running) 7%, transparent),
transparent
);
animation: fx-scan 2.8s var(--ease-in-out) infinite;
}
.nt-supervisor .fx-scan {
--scan-h: 220px;
}
/* 刚跑完:绿色扫过一遍。只跑一次,跑完停在画面外——
常驻的完成动画会让整张画布一直在动,而"完成了"这件事只需要说一次 */
@keyframes fx-done {
from { transform: translateX(-110%); }
to { transform: translateX(110%); }
}
.fx-done,
.fx-sweep {
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent,
color-mix(in srgb, var(--st-done) 28%, transparent),
transparent
);
transform: translateX(-110%);
}
.fx-done {
animation: fx-done 0.85s var(--ease-out) 1 forwards;
}
/* 刚失败:短促地抖一下。幅度压在 3px 以内——再大就不像"出错"像"地震" */
@keyframes fx-shake {
0%, 100% { transform: translateX(0); }
15% { transform: translateX(-3px); }
30% { transform: translateX(3px); }
45% { transform: translateX(-2px); }
60% { transform: translateX(2px); }
80% { transform: translateX(-1px); }
}
.nc.node-failed.nc-enter {
animation: fx-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) 1;
}
/* 一次性时刻共用的那层描边。平时透明,由下面几种时刻各自点亮一次 */
.fx-moment {
position: absolute;
inset: -1px;
border-radius: inherit;
opacity: 0;
}
@keyframes nc-light {
0% { opacity: 0; }
30% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes nc-ring-out {
from { opacity: 0.9; transform: scale(1); }
to { opacity: 0; transform: scale(1.06, 1.14); }
}
/* 刚停下来等人:琥珀色往外荡一圈,然后静止。停多久都不再动 */
.nc.node-waiting.nc-enter .fx-moment {
box-shadow: 0 0 0 2px var(--st-waiting);
animation: nc-ring-out 900ms var(--ease-out) 1 both;
}
/* 开场点亮:run.started 时沿拓扑层级依次亮一下(--rank 由画布写到节点上) */
.react-flow[data-moment='start'] .fx-moment {
box-shadow: inset 0 0 0 1px var(--st-running),
0 0 14px -2px color-mix(in srgb, var(--st-running) 55%, transparent);
animation: nc-light 520ms var(--ease-out) 1 both;
animation-delay: calc(var(--rank, 0) * 55ms);
}
/* 成功回扫:执行路径上的节点从入口到成果依次扫一遍 */
.react-flow[data-moment='success'] .nc[data-state='done'] .fx-sweep {
animation: fx-done 700ms var(--ease-out) 1 both;
animation-delay: calc(var(--rank, 0) * 70ms);
}
.react-flow[data-moment='success'] .nc[data-state='done'] .fx-moment {
box-shadow: inset 0 0 0 1px var(--st-done);
animation: nc-light 600ms var(--ease-out) 1 both;
animation-delay: calc(var(--rank, 0) * 70ms);
}
/* 失败阻断级联:从失败节点往下游逐层标「阻断」,一层 60ms。
--moment-origin 是失败节点的层级(画布写在根元素上),没有就从 0 算 */
@keyframes nc-block {
from { opacity: 1; }
}
.react-flow[data-moment='failed'] .nc[data-state='blocked'] {
animation: nc-block 420ms var(--ease-out) 1 both;
animation-delay: calc(max(0, var(--rank, 0) - var(--moment-origin, 0)) * 60ms);
}
.react-flow[data-moment='failed'] .nc[data-state='blocked'] .fx-moment {
box-shadow: inset 0 0 0 1px var(--st-blocked);
animation: nc-light 420ms var(--ease-out) 1 both;
animation-delay: calc(max(0, var(--rank, 0) - var(--moment-origin, 0)) * 60ms);
}
/* 右栏点了某一步、画布取景过来:落点的节点荡一圈,告诉眼睛"是这个" */
.fx-focus {
position: absolute;
inset: -1px;
border-radius: inherit;
box-shadow: 0 0 0 2px var(--accent);
animation: nc-ring-out 700ms var(--ease-out) 1 both;
}
/* --------------------------------------------------------------------------
协作矩阵:supervisor 节点展开出来的内部编排。
它要回答三个问题:这一轮派了几个人、谁还在跑、并行到底省了多少。
条形长度按"这一轮里最长的那个人"归一化——三个人并排三条长短不一的条,
一眼就能看出墙钟为什么等于最长那条而不是三条之和。
行高固定,行数从编辑态起就固定,跑起来卡片不长个子。
-------------------------------------------------------------------------- */
.team-matrix {
padding: 3px 10px 0;
}
.team-head {
display: flex;
align-items: center;
gap: 6px;
height: 17px;
color: var(--text-faint);
font-size: 9.5px;
letter-spacing: 0.02em;
white-space: nowrap;
}
.team-head-rule {
flex: 1;
min-width: 6px;
height: 1px;
background: linear-gradient(to right, var(--hairline), transparent);
}
.team-head-right {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
/* 用完轮数仍未完成:失败是红、降档交付是琥珀。平常的表头是安静的灰 */
.team-head-right.is-err {
color: var(--st-failed);
font-weight: 600;
}
.team-head-right.is-warn {
color: var(--st-waiting);
font-weight: 600;
}
.team-row {
position: relative;
display: flex;
align-items: center;
gap: 6px;
height: 18px;
padding-left: 5px;
border-left: 2px solid transparent;
font-size: 10.5px;
}
/* 被派出去的成员:左边一条竖线把它们串成一组。在跑时是运行色,收尾之后
退成中性线——蓝色只说「正在跑」 */
.team-row-active {
border-left-color: var(--border-strong);
}
.team-matrix.is-live .team-row-active {
border-left-color: color-mix(in srgb, var(--st-running) 55%, transparent);
}
.team-row-running .team-name {
color: var(--text);
}
.team-row-idle {
opacity: 0.5;
}
/* 前几轮干过、这一轮没它:压暗但留着成绩,不是"待命" */
.team-row-past {
opacity: 0.6;
}
/* 轮数用完、一次都没被派到:不压暗,左边一道琥珀虚线——它正是这次没做完的线索 */
.team-row-never {
border-left: 2px dashed color-mix(in srgb, var(--st-waiting) 70%, transparent);
}
.team-row-never .team-name,
.team-row-never .team-ms {
color: var(--st-waiting);
}
/* 调度者那一行:和成员分开,底下一道细线 */
.team-row-dispatch {
margin-bottom: 2px;
border-bottom: 1px dashed var(--hairline);
}
.team-row-dispatch .team-name {
color: var(--text-faint);
}
.team-row-dispatch.team-row-running .team-name {
color: var(--text);
}
.team-name {
flex: none;
width: 58px;
overflow: hidden;
color: var(--text-dim);
text-overflow: ellipsis;
white-space: nowrap;
}
.team-bar {
position: relative;
flex: 1;
min-width: 18px;
height: 3px;
overflow: hidden;
border-radius: 999px;
background: var(--bg-hover);
}
/* 长度用 scaleX 而不是 width:动效只走 transform */
.team-bar-fill {
position: absolute;
inset: 0;
border-radius: 999px;
background: color-mix(in srgb, var(--st-done) 80%, transparent);
transform-origin: left center;
transition: transform var(--dur-slow) var(--ease-out);
}
.team-row-failed .team-bar-fill {
background: var(--st-failed);
}
/* 还在跑:不知道要多久,所以是不定长的流光,不是百分比 */
.team-bar-running {
position: absolute;
top: 0;
bottom: 0;
width: 40%;
border-radius: 999px;
background: linear-gradient(90deg, transparent, var(--st-running), transparent);
animation: team-shimmer 1.15s var(--ease-in-out) infinite;
}
@keyframes team-shimmer {
from { transform: translateX(-110%); }
to { transform: translateX(260%); }
}
.team-ms {
flex: none;
min-width: 38px;
color: var(--text-faint);
font-size: 9.5px;
text-align: right;
white-space: nowrap;
}
.team-row-running .team-ms {
color: var(--st-running);
}
.team-row-failed .team-ms {
color: var(--st-failed);
}
.team-foot {
display: flex;
align-items: center;
gap: 5px;
height: 18px;
margin-top: 2px;
border-top: 1px dashed var(--hairline);
color: var(--text-faint);
font-size: 9.5px;
}
.team-saved {
color: var(--st-done);
font-weight: 500;
}
/* 活跃边上跑的光点:方向感靠它,虚线只会让人觉得"在动",看不出往哪走。
边的运行态(走过、正在流、停在审批、阻断)在 canvas/surface.css,这里只留
光点的底样式和它的关键帧,surface.css 在画布根上覆盖颜色 */
.edge-packet {
fill: var(--accent);
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent) 80%, transparent));
offset-rotate: 0deg;
animation-name: packet-run;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@keyframes packet-run {
from { offset-distance: 0%; }
to { offset-distance: 100%; }
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
.fade-up {
animation: fade-up 0.16s ease-out;
}
/* --------------------------------------------------------------------------
助手栏的动效。
缓动统一用 --ease-out:起步快、尾部长,是"被推出来"而不是"匀速平移"的
手感。面板滑动用 --dur-base——超过 240ms 就开始像卡顿,而不是像流畅。
-------------------------------------------------------------------------- */
/* 属性面板:从右侧盖到助手栏上。它是临时造访者,助手才是常住的,所以用
"盖上去再拿走"而不是切 tab——切 tab 会把下面那层卸载掉,滚动位置、
输入草稿、展开状态全丢。 */
@keyframes sheet-in {
from { opacity: 0; transform: translateX(16px); }
to { opacity: 1; transform: none; }
}
.sheet-in {
animation: sheet-in var(--dur-base) var(--ease-out);
}
/* --------------------------------------------------------------------------
可点击证据(run/EvidenceDoc)。报告里有出处、没出处的片段是原生 button,外观
和正文一样;状态靠下划线的线型和颜色,外观数据在 lib/evidence.ts(颜色、淡底
由组件写成 --ev-line / --ev-soft),这里只管线型和交互态。
不画整句下划线:一份报告满屏都是线就什么都看不出来了。
-------------------------------------------------------------------------- */
.ev-seg {
display: inline;
margin: 0;
padding: 0;
border: 0;
border-radius: 2px;
background: none;
font: inherit;
color: inherit;
letter-spacing: inherit;
line-height: inherit;
text-align: inherit;
white-space: inherit;
cursor: pointer;
text-decoration-line: underline;
text-decoration-color: var(--ev-line, currentColor);
text-decoration-thickness: 1px;
text-underline-offset: 3px;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
.ev-seg[data-ev-line="solid"] { text-decoration-style: solid; }
/* 点状线比实线细碎,同样 1px 在小字号下几乎看不见 */
.ev-seg[data-ev-line="dotted"] { text-decoration-style: dotted; text-decoration-thickness: 1.5px; }
/* 引文:前面挂一个引号(字形那一条通道),颜色随状态——有出处的绿、原话对不上的琥珀。
引号是装饰,读屏从 aria-label 里听到「引文」,这里的替代文字给空串 */
.ev-seg[data-ev-kind="quote"]::before {
content: "“";
content: "“" / "";
margin-right: 1px;
color: var(--ev-glyph, currentColor);
font-weight: 600;
}
/* 旧运行的候选:比无证据的点状线更淡、更细,绝不像确定性的实线。正文里的候选是 Markdown 的标记(.ev-mark),
不是按钮:旧答案保留原来的版式,候选的明细在下面的清单里逐条列出 */
.ev-seg[data-ev-state="candidate"] { text-decoration-thickness: 1px; }
.ev-mark {
text-decoration-line: underline;
text-decoration-style: dotted;
text-decoration-thickness: 1px;
text-decoration-color: var(--ev-line, currentColor);
text-underline-offset: 3px;
}
.ev-seg:hover,
.ev-seg[aria-expanded="true"] { background: var(--ev-soft, transparent); }
.ev-seg:focus-visible,
.ev-seg[data-flash] {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* 句末的「无证据」小标签:字形加文字,去掉颜色也读得出 */
.ev-tag {
margin: 0 3px;
font-size: 10px;
line-height: 1;
white-space: nowrap;
vertical-align: 1px;
color: var(--st-waiting);
}
/* 结论句的判定(四期):句末上标的小字形,是按钮但不像按钮。颜色随判定(组件写成 --ev-glyph),
绝不用确定性的绿;整句的字不画线——判定是模型给的,不能长得像系统核对过的数字 */
.ev-badge {
display: inline-block;
margin: 0 1px 0 2px;
padding: 0 3px;
border: 0;
border-radius: 3px;
background: none;
font: inherit;
font-size: 0.72em;
font-weight: 700;
line-height: 1;
vertical-align: super;
color: var(--ev-glyph, currentColor);
text-decoration: none;
cursor: pointer;
}
.ev-badge:hover,
.ev-badge[aria-expanded="true"] { background: var(--ev-soft, transparent); }
.ev-badge:focus-visible,
.ev-badge[data-flash] {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* 证据不支持:整句铺一层浅底,折行时每一行各自带圆角;不画线、不改字色 */
.ev-claim {
background: var(--ev-soft, transparent);
border-radius: 2px;
text-decoration: none;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
.ev-panel-side { animation: sheet-in var(--dur-base) var(--ease-out); }
.ev-panel-rise { animation: fade-up var(--dur-base) var(--ease-out); }
/* 例句卡片依次入场。--i 是序号,错开 40ms 就够读出"一条条出现",
再多就开始等 */
@keyframes rise-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.rise-in {
opacity: 0;
animation: rise-in var(--dur-slow) var(--ease-out) forwards;
animation-delay: calc(var(--i, 0) * 40ms);
}
/* 正在流的那一步:一道光扫过文字。比转圈多说一件事——它在出字,不只是在等 */
@keyframes shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
.shimmer {
background: linear-gradient(
90deg,
var(--text-faint) 0%, var(--text-faint) 35%,
var(--text) 50%,
var(--text-faint) 65%, var(--text-faint) 100%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: shimmer 2.4s linear infinite;
}
/* 空态那枚标记出现时"吸一口气":暗示可以开始了。只播一次就停——空闲时
一直在动的东西会一直分走注意力,而这里什么都没发生 */
@keyframes breathe {
0% { opacity: 0.55; transform: scale(1); }
45% { opacity: 1; transform: scale(1.06); }
100% { opacity: 1; transform: none; }
}
.breathe {
animation: breathe 2.4s var(--ease-in-out) 1 both;
}
/* 输入坞聚焦时的光晕。焦点是这一栏最重要的状态,值得比 1px 描边更明确 */
.dock-focus {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
border-color: var(--accent);
}
/* --------------------------------------------------------------------------
前庭敏感的人看这些动效会难受,而这套界面里动的东西不少(边在流动、节点在
脉冲、面板在滑)。系统里关掉动效的,这里就全部停掉——不是调慢,是停掉。
-------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.rise-in { opacity: 1; }
.shimmer {
background: none;
-webkit-background-clip: border-box;
background-clip: border-box;
color: var(--text-dim);
}
/* 边框上的光弧、扫描线、扫光、一次性时刻的描边、流光条、边上的光点——
这些"停了就等于没有"的直接不画。只把时长压到 0.01ms 的话它们会停在动画
的最后一帧:光弧卡在角落、光点全堆在起点,看着像渲染坏了。
状态不会因此看不出来:卡片还有剪影、左侧状态槽、边框线型和状态文字,
在跑的节点和成员照样走着计时 */
.fx-halo,
.fx-scan,
.fx-done,
.fx-sweep,
.fx-moment,
.fx-focus,
.edge-packet,
.team-bar-running {
display: none;
}
/* 时钟这时 1 秒才走一拍,十分位只会一顿一顿地跳 */
.nc-tenth {
display: none;
}
/* 证据面板直接出现,不滑入:滑入停在 0.01ms 的最后一帧虽然看不出来,但它仍然是一段动画 */
.ev-panel-side,
.ev-panel-rise {
animation: none !important;
}
}
/* --------------------------------------------------------------------------
组件类放进 components 层:排在 utilities 前面,元素上再写的工具类
(w-16、text-[12px]、border-[var(--accent)]…)才压得过它们。以前它们不在
任何层里,这些工具类全被静默吞掉——`field w-44` 实际占满整行。
-------------------------------------------------------------------------- */
@layer components {
/* 表单控件统一外观 */
.field {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 6px 9px;
color: var(--text);
font-size: 12px;
outline: none;
transition: border-color var(--dur-fast) var(--ease-out);
}
.field:focus {
border-color: var(--accent);
}
.field::placeholder {
color: var(--text-faint);
}
textarea.field {
resize: vertical;
min-height: 62px;
line-height: 1.55;
}
select.field {
cursor: pointer;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: var(--radius-md);
padding: 6px 11px;
font-size: 12px;
font-weight: 500;
border: 1px solid var(--border);
background: var(--bg-elev);
color: var(--text);
cursor: pointer;
/* 按属性列出来:transition: all 会把布局属性也带上,主题切换、尺寸变化
时一起慢慢挪 */
transition-property: background-color, border-color, color, box-shadow, opacity;
transition-duration: var(--dur-fast);
transition-timing-function: var(--ease-out);
white-space: nowrap;
}
.btn:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-strong);
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
/* 实心底用 --accent-solid 配 --on-accent:深色下白字压在 --accent 上只有
3.2:1。悬停压暗而不是提亮——提亮会把对比度拉回 4.5 以下 */
.btn-primary {
background: var(--accent-solid);
border-color: var(--accent-solid);
color: var(--on-accent);
}
.btn-primary:hover:not(:disabled) {
background: color-mix(in srgb, var(--accent-solid) 88%, #000);
border-color: color-mix(in srgb, var(--accent-solid) 88%, #000);
}
.btn-danger {
color: var(--err);
border-color: color-mix(in srgb, var(--err) 40%, transparent);
}
.btn-ghost {
background: transparent;
border-color: transparent;
}
.btn-ghost:hover:not(:disabled) {
background: var(--bg-hover);
}
.btn-sm {
padding: 3px 7px;
font-size: 11px;
}
/* 行内的小图标按钮(会话列表的重命名 / 删除、气泡下的小操作)。以前用了
七处却没定义,实际按普通按钮的尺寸渲染 */
.btn-xs {
padding: 2px 6px;
font-size: 11px;
gap: 4px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 7px;
border-radius: 999px;
font-size: 11px;
border: 1px solid var(--border);
color: var(--text-dim);
white-space: nowrap;
}
.label {
display: block;
font-size: 11px;
color: var(--text-dim);
margin-bottom: 4px;
font-weight: 500;
}
}