Skip to content

基础件目录

primitives / forms / layout / feedback / data 五层的组件与 props。 图表见 charts.md,整块看板见 boards.md

约定:? = 可选 prop; = $bindable;snippet 参数写在类型里。


primitives —— 原子件

Icon

统一的线性图标集(Lucide 派生,MIT)。24px 网格、1.75 描边、currentColor——尺寸与 颜色由调用点决定,这样整套 UI 共享同一套图标语言,而不是各处凑字形。

svelte
<Icon name="nodes" size={16} />

IconName 是一个联合类型(约 40 个名字,从 dashboardnodeswireguardbird)。加图标要改这个类型 + PATHS,这是有意的摩擦:图标集应当有人把关。

wireguard 是唯一按实心填充绘制的(品牌标),其余都是描边图标。

Tooltip

包一层 bits-ui 的 tooltip。

prop类型说明
labelstring提示文字
enabled?booleanfalse裸渲染 trigger、不挂提示——用于「只在侧栏折叠时才提示」
side?'top'|'right'|'bottom'|'left'默认 right
triggerSnippet<[Record<string, unknown>]>收到要展开到自己元素上的 props

trigger 是 snippet 而不是 children,这样真正的触发元素仍然是调用方的 <a> / <button>——语义与键盘可达性不被包装层吃掉。

需要树上有一个 <Tooltip.Provider>AppShell 已经提供)。

IconButton

顶栏图标控件的唯一形态(全局 30px .iconbtn 结构)。总是带 Tooltip; 传 choices 时点击展开 bits-ui 的 DropdownMenu,项按 value 单选打勾。

prop类型说明
iconIconName
labelstringtooltip 文字 + aria-label
side?方位默认 bottom
active?boolean开关态,渲染 aria-pressed(仅普通模式)
spin?boolean一次性旋转动效
off?boolean「已关闭」外观:淡化 + 斜杠
choices?IconChoice[]菜单模式的选项
value?string菜单模式的当前选中项(打 ✓)
onselect? / onclick?回调菜单 / 普通模式

它统一了此前分裂的三套行为:自动刷新开关(Tooltip 化的 toggle)、语言切换器 (原生 title 的 Select trigger)、主题切换器(CSS hover 展开的菜单)。一套交互语法。

指向 DN42 registry explorer 的外链。explorer URL 模板与外链卫生(target / rel) 只在这一处。

prop类型说明
asnnumber
name?string | null运营者名
showName?boolean渲染名字(回落 AS{asn})而不是号码
title? / cls?string原生 title / 额外类名(截断助手等)

HeadInfo

表格列头的「ⓘ」提示。悬停或聚焦展示该指标的口径定义。

svelte
<th>抖动分 <HeadInfo label={t('pflap.scoreTip')} /></th>

存在的理由:让 th 的文字继续承担排序与标签职责,把「这个数是什么意思」交给图标, 不让列头变成一大段解释。


forms —— 表单控件

SegControl

分段视图切换器(Radar 卡角的「两者 / IPv4 / IPv6」控件)。

ts
options: { value: string; label: string }[];
value: string;
onchange: (v: string) => void;

值一律是 string——联合类型的调用方在自己的 onchange 里转型。理由见 contracts.md 准则四

Toggle

带标签的开关(Radar 的橙色「最小/最大比例」开关):左文字、右滑轨。

ts
label: string;
checked?: boolean;   // ⇄ bindable

CapCheck

人机验证控件。解题引擎是 vendored 的上游 Cap,界面全部是自绘的,用设计令牌画, 和库里其他控件一样。

prop类型说明
endpointstringCap 实例 + site key,含结尾斜杠
labels对象initial / verifying / solved / error / blocked? / loadFailed。文案归宿主
onToken(token: string) => void解出票时带 token 触发;reset / 出错时带 ''
onLoadError?() => void解题器 chunk 取不到时触发一次

自绘界面换来的三件事:不必透过第二套 CSS 变量去 theme 一个 shadow root;切语言不重挂 (文案是普通 prop);没有字标要对抗。

⚠️ 票据一次性,前端不做预校验(会烧掉 token)。背景见 internals/captcha.md


layout —— 容器与外壳

AppShell

应用外壳:整行顶栏(品牌永远不随侧栏折叠)+ 可折叠侧栏(图标轨 / hover 暂展开 / 手机抽屉)+ 内容区。控制台与门户共用同一份,两个站的骨架、间距、断点因此不会各自漂移。

prop类型说明
navNavItem[]children 的是可折叠分组,否则是叶子链接
pathnamestring当前路径。库不能 import $app/state,由宿主传
crumbs?{label, href?}[]面包屑;最后一项渲染为当前页(不可点)
pageDesc?string页面描述,挂在面包屑旁的 info 气泡里;空则不渲染
brandHref? / brandLabel?string默认 / / NATLAN
collapsed?boolean侧栏折叠态。宿主自己持久化(两站的 localStorage 键不同)
logo? / actions? / sidebarFoot?Snippet品牌标 / 顶栏右侧控件 / 侧栏底部
childrenSnippet内容区
ts
type NavLeaf = { href: string; key: string };
type NavItem = { key: string; icon: IconName; href?: string; children?: NavLeaf[] };

key 是 i18n key(nav.overview 之类),走 t()——库字典查不到会回落到宿主字典。 这正是「通用词汇归 app」那条分工的用处。

hover peek:折叠成图标轨后,悬停把侧栏以浮层展开(脱离文档流,内容列保持 64px 偏移),移开还原。只有折叠按钮会真正改变布局宽度。用折叠按钮收起后若指针还停在 侧栏内,要等它离开过一次才允许 peek——否则一收起就立刻又展开。

Widget

Radar widget 卡片——每个仪表盘区块都用这一种解剖结构

┌ 标题 (+ 可选 ↗ 深链)              [控制簇] ┐
│ 描述                                        │
│ …内容…                                      │
└ 更新于 …(可选)                            ┘
prop类型说明
titlestring
sub?string标题下的一行灰色描述
count?number标题后的淡色 (n)
icon?IconName
href?string深链,渲染 Radar 式的橙色 ↗ 跳转箭头
asof?string左下角「更新于 …」戳
cls?string页面级尺寸控制的额外类名
controls?Snippet右上控制簇(分段控件、下拉、按钮)
childrenSnippet内容

标题 / 描述 / 控制簇 / as-of 的位置在这里被钉死,页面不再各自手写头部行。

asof 不是装饰:dashboard 有服务端缓存、routing 采集有分钟级延迟,「这个数字是几点的」 直接决定可信度。

CardHead

Widget 的轻量兄弟——卡的分节标题。节点详情的各页签与面板用它。

prop类型说明
titlestring
count?number标题后的淡色 (n)
level?3 | 4标题层级,嵌套子节用 4
icon?IconName前置强调图标
loading?boolean取数中禁用刷新按钮
onrefresh?() => void传了才渲染标准的图标刷新按钮
controls?Snippet额外右侧控件

选哪个:整块仪表盘卡片用 Widget;卡片内部或页签内部的分节用 CardHead


feedback —— 状态反馈

StatusBadgeHealthBadge

两个不能互换:

StatusBadgeHealthBadge
知不知道词汇表不知道知道
输入cls(色调)+ label(成品文案)value(后端原值)
谁做映射调用点(页面特定的业务逻辑)组件(内建两张表 + 本地化)
用在哪任意状态胶囊机群 health / report·apply status
svelte
<StatusBadge cls="warn" label={t('flaps.middleBand')} />
<HealthBadge value={node.health} muted={node.liveness === 'offline'} />

HealthBadge 内建两张表:health(ok / stale / degraded / down / unknown / bad)与 status(succeeded / failed / skipped / running / pending), 无法识别的值回落成中性 + 原样显示。

muted 把徽章淡化,用于表示「这是陈旧的 / 最后已知值」(例如已断连节点的 report 状态)。

LiveDot

活性圆点,用图表色板(--c-*)着色,因此与地图标记、状态图在两个主题下都一致。

ts
status?: 'ok' | 'stale' | 'down' | 'unknown';   // 默认 unknown
size?: number;   // 默认 7
title?: string;

InlineBanner

图标 + 着色细条。默认形态(不传任何 prop)就是「刷新失败——显示最后已知数据」, 即 keep-last-good 在陈旧内容上方渲染的那条。

prop类型说明
message?string默认为 stale-data 文案
detail?string消息后的截断灰字(错误原文,悬停看全)
tone?'warn' | 'bad' | 'ok' | 'neutral'默认 warn,决定默认图标
icon?IconName覆盖默认图标
children?Snippet富文本消息,优先于 message

这是交互契约 2 与 5 的落点:加载错误走就地 banner,数据保留,而不是用错误卡片 替换整张表。见 internals/interaction-contracts.md

EmptyState

居中图标 + 标题 + 提示 + 可选 CTA。用于「暂无数据」占位,不用于临时加载文案。

ts
icon?: IconName;   // 默认 'dashboard'
title: string;
hint?: string;
actionLabel?: string;
onaction?: () => void;

契约 6 规定:空状态只允许用它,不写裸 .empty div。

抖动板块的空态要写成正向文案(「近期无会话抖动」「全网路由平稳」)——空列表在那里 是机群健康的信号,不是缺数据。

Skeleton 四件套

组件形状主要 props
Skeleton单个微光块w / h / radius / circle
SkeletonRowsN 条等宽横条count(默认 6)/ h / gap
SkeletonText段落形(最后一行短)lines(默认 3)/ gap / height
SkeletonTable表格形(可选表头 + 按列宽的单元格)headers? / cols(长度即列数)/ rows

SkeletonTablecols 传每列的 CSS 宽度,让占位与真实列宽对齐——加载态因此能平滑 交叉淡入到内容,而不是跳变。

契约 6 规定:骨架屏只允许这四个,不手写骨架行。


data —— 表格与分页

AsRankTable

Radar 的「前 100 个 AS」排名表,做成可复用的: [排序依据分段控件] + [图标搜索] + 排名/ASN/名称/指标列 + ⏮◀▶⏭ 分页 + as-of 戳。 筛选、排序、分页全在客户端。

两种外壳:传 title 渲染成完整的 Widget 卡片(控制簇落在卡头,与 Radar 一致); 省略则是裸表格,嵌进别的卡片里(例如图表旁边)。

prop类型说明
rowsT[]全量行集,组件自己筛选/排序/分页
metricsAsMetric<T>[]≥1;>1 时出现分段控件,激活的指标驱动列头、取值与排序
title?string传了就包 Widget 卡
sub?(metricLabel) => string卡片描述,随激活指标变
searchable?boolean图标搜索框;同时匹配 ASN 与名称,并会剥掉输入里的 AS 前缀
pageSize?number默认 20
edges?boolean分页加首页/末页跳转(Radar 大表变体)
asof? / loading? / emptyText?
headerExtra?Snippet额外卡头控件
ts
interface AsRow { asn: number; name: string | null; }
interface AsMetric<T> { key: string; label: string; value: (r: T) => number; format?: (v: number) => string; }

name 应当是注册表 as-name(权威),调用方在传入前解析好。

Pager

共享分页器(Radar Top-N 表脚):⏮ ◀ ▶ ⏭ + 「第 x / y 页」。

ts
page: number;      // 零基
pages: number;
onchange: (p: number) => void;
edges?: boolean;   // 首/末跳转
label?: string;    // 前置元信息(如「共 X 条」)

LoadMore

追加式列表(审计日志、状态事件)的居中游标分页脚。Pager 分工明确Pager 管页码导航,LoadMore 管「加载更早」。

ts
hasMore: boolean;
loading: boolean;   // 列表的 loadingMore 标志,不是首载标志
onload: () => void;
label: string;

loading 必须传独立的「加载更多中」标志——用首载标志会让整张表在追加时闪成骨架。