Files
MoShiro/web/shared/theme/base.css
T
justme a2f88a344b
ci/woodpecker/tag/gitea-release Pipeline is pending
ci/woodpecker/tag/linux-release Pipeline failed
ci/woodpecker/tag/windows-release Pipeline was canceled
feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
Join codes need no passkey touch and carry only public values (genesis
fingerprint, owner credentials, installer hashes). Nodes pin the genesis and
owners at enrollment and only follow passkey-signed operator set versions.
The panel's server key is listed in the set; pinned nodes require its
signature and run the control link under AES-256-GCM. Password sessions carry
no capabilities. The updater adopts a next_key from a signed manifest.

Bump version to 0.2.0. [CI SKIP]
2026-10-08 03:21:44 +08:00

1081 lines
18 KiB
CSS

@import "./tokens.css";
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
background: var(--app-bg);
color: var(--app-ink);
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
h1,
h2,
h3,
h4,
p {
margin: 0;
}
a {
color: var(--app-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button,
input,
select,
textarea {
font: inherit;
color: inherit;
}
.mono {
font-family: var(--font-mono);
font-size: 0.92em;
}
.muted {
color: var(--app-ink-muted);
}
.faint {
color: var(--app-ink-faint);
}
.num {
font-variant-numeric: tabular-nums;
}
.row {
display: flex;
align-items: center;
gap: 12px;
}
.row.wrap {
flex-wrap: wrap;
}
.grow {
flex: 1;
min-width: 0;
}
.stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.right {
margin-left: auto;
}
/* ── 卡片:靠色调分层,没有描边 ── */
.card {
background: var(--app-card);
border-radius: var(--r-card);
padding: 20px 24px;
}
.card-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.inset {
background: var(--app-card-inset);
border-radius: var(--r-control);
padding: 12px 16px;
}
.divider {
height: 1px;
background: var(--app-outline-soft);
border: 0;
margin: 16px 0;
}
/* ── 按钮 ── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 18px;
border: 0;
border-radius: var(--r-pill);
background: var(--app-card-inset);
color: var(--app-ink);
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
.btn:hover:not(:disabled) {
background: color-mix(in srgb, var(--app-card-inset), var(--app-ink) calc(var(--state-hover) * 100%));
}
.btn:disabled {
opacity: 0.45;
cursor: default;
}
.btn.primary {
background: var(--app-primary);
color: var(--app-on-primary);
}
.btn.primary:hover:not(:disabled) {
background: color-mix(in srgb, var(--app-primary), var(--app-on-primary) calc(var(--state-hover) * 100%));
}
.btn.accent {
background: var(--app-accent);
color: var(--app-card);
}
.btn.text {
background: transparent;
color: var(--app-accent);
padding: 0 12px;
}
.btn.text:hover:not(:disabled) {
background: color-mix(in srgb, var(--app-accent), transparent calc((1 - var(--state-hover)) * 100%));
}
.btn.danger {
background: var(--app-bad-container);
color: var(--app-bad);
}
.btn.icon {
width: 36px;
padding: 0;
}
.btn.small {
height: 30px;
padding: 0 12px;
font-size: 13px;
}
/* ── 连接式分组(M3 Expressive connected button group):组内小圆角、外角大圆角、选中段变胶囊 ── */
.group {
display: inline-flex;
gap: 2px;
}
.group > button {
height: 36px;
padding: 0 16px;
border: 0;
background: var(--app-card-inset);
color: var(--app-ink-muted);
font-weight: 500;
cursor: pointer;
border-radius: var(--r-group-inner);
transition:
border-radius 0.18s,
background 0.15s;
}
.group > button:first-child {
border-radius: var(--r-group-outer) var(--r-group-inner) var(--r-group-inner) var(--r-group-outer);
}
.group > button:last-child {
border-radius: var(--r-group-inner) var(--r-group-outer) var(--r-group-outer) var(--r-group-inner);
}
.group > button:only-child {
border-radius: var(--r-group-outer);
}
.group > button:hover {
background: color-mix(in srgb, var(--app-card-inset), var(--app-ink) calc(var(--state-hover) * 100%));
}
.group > button.on {
background: var(--app-accent-container);
color: var(--app-on-accent-container);
border-radius: var(--r-group-outer);
}
/* ── 表单 ── */
.field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.field > label,
.field > .label {
font-size: 12px;
color: var(--app-ink-muted);
}
.field .hint {
font-size: 12px;
color: var(--app-ink-faint);
}
.input,
.select,
.textarea {
width: 100%;
min-height: 40px;
padding: 8px 14px;
border: 0;
border-radius: var(--r-control);
background: var(--app-card-inset);
outline: 2px solid transparent;
outline-offset: -2px;
transition: outline-color 0.15s;
}
.textarea {
resize: vertical;
font-family: var(--font-mono);
font-size: 13px;
}
.input:focus,
.select:focus,
.textarea:focus {
outline-color: var(--app-accent);
}
.input::placeholder {
color: var(--app-ink-faint);
}
.check {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.check input {
accent-color: var(--app-accent);
width: 16px;
height: 16px;
}
/* ── 小件 ── */
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
height: 24px;
padding: 0 10px;
border-radius: var(--r-pill);
background: var(--app-card-inset);
color: var(--app-ink-muted);
font-size: 12px;
white-space: nowrap;
}
.chip.ok {
background: var(--app-ok-container);
color: var(--app-ok);
}
.chip.warn {
background: var(--app-warn-container);
color: var(--app-warn);
}
.chip.bad {
background: var(--app-bad-container);
color: var(--app-bad);
}
.chip.accent {
background: var(--app-accent-container);
color: var(--app-on-accent-container);
}
.dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--app-idle);
}
.dot.ok {
background: var(--app-ok);
}
.dot.warn {
background: var(--app-warn);
}
.dot.bad {
background: var(--app-bad);
}
/* ── 表格:细分隔线,没有边框 ── */
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
text-align: left;
font-weight: 500;
font-size: 12px;
color: var(--app-ink-faint);
padding: 8px 12px;
border-bottom: 1px solid var(--app-outline);
white-space: nowrap;
}
.table td {
padding: 12px;
border-bottom: 1px solid var(--app-outline-soft);
vertical-align: middle;
}
.table tr:last-child td {
border-bottom: 0;
}
.table.hover tbody tr {
cursor: pointer;
}
.table.hover tbody tr:hover td {
background: var(--app-card-hover);
}
.table th.r,
.table td.r {
text-align: right;
}
/* ── 指标块 ── */
.stat {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.stat .k {
font-size: 12px;
color: var(--app-ink-faint);
}
.stat .v {
font-size: 24px;
font-weight: 600;
line-height: 1.2;
font-variant-numeric: tabular-nums;
}
.stat .v small {
font-size: 12px;
font-weight: 400;
color: var(--app-ink-muted);
margin-left: 2px;
}
.stat .v.ok {
color: var(--app-ok);
}
.stat .v.warn {
color: var(--app-warn);
}
.stat .v.bad {
color: var(--app-bad);
}
.grid {
display: grid;
gap: 16px;
}
.grid.cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid.cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid.cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid.stats {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
@media (max-width: 900px) {
.grid.cols-2,
.grid.cols-3,
.grid.cols-4 {
grid-template-columns: minmax(0, 1fr);
}
}
/* ── 对话框与提示 ── */
.scrim {
position: fixed;
inset: 0;
background: color-mix(in srgb, var(--app-scrim), transparent 55%);
backdrop-filter: blur(3px);
display: grid;
place-items: center;
z-index: 50;
padding: 16px;
}
.dialog {
background: var(--app-card);
border-radius: 28px;
padding: 24px;
width: min(560px, 100%);
max-height: calc(100vh - 32px);
overflow: auto;
border: 1px solid var(--app-outline-soft);
/* 近处一圈细的、远处一大片柔的:边缘清楚,又不会糊成一团 */
box-shadow:
0 1px 2px color-mix(in srgb, var(--app-shadow), transparent 80%),
0 12px 40px color-mix(in srgb, var(--app-shadow), transparent 72%);
}
.dialog h2 {
font-size: 20px;
margin-bottom: 16px;
}
/* 宽对话框:标题和按钮固定,中间滚动(内容很长的表单,比如选能力) */
.dialog.wide {
width: min(980px, 100%);
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog.wide .dialog-body {
flex: 1;
min-height: 0;
overflow: auto;
margin: 0 -8px;
padding: 0 8px;
}
.dialog.wide .actions {
margin-top: 16px;
}
/* 能力选择器:工具栏吸顶,分组排成多列,每项两行(名称,代码和说明) */
.cap-toolbar {
position: sticky;
top: 0;
z-index: 2;
background: var(--app-card);
padding-bottom: 10px;
}
.cap-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
align-items: start;
}
.cap-group {
border: 1px solid var(--app-outline-soft);
border-radius: 16px;
padding: 12px 14px;
}
.cap-row {
padding: 5px 0 5px 22px;
border-top: 1px solid var(--app-outline-soft);
}
.cap-row:first-of-type {
border-top: 0;
}
.cap-meta {
margin: 1px 0 0 26px;
font-size: 12px;
line-height: 1.45;
}
.cap-meta .mono {
margin-right: 6px;
}
.dialog .actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
}
.toasts {
position: fixed;
right: 24px;
bottom: 24px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 60;
}
.toast {
background: var(--app-primary);
color: var(--app-on-primary);
border-radius: var(--r-control);
padding: 10px 16px;
max-width: 420px;
box-shadow: 0 4px 16px color-mix(in srgb, var(--app-shadow), transparent 75%);
}
.toast-title {
font-weight: 700;
margin-bottom: 2px;
}
.toast {
cursor: pointer;
}
.toast.bad {
background: var(--app-bad);
color: var(--app-card);
}
.empty {
padding: 40px 16px;
text-align: center;
color: var(--app-ink-faint);
}
.notice {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 14px 18px;
border-radius: var(--r-control);
background: var(--app-accent-container);
color: var(--app-on-accent-container);
}
.notice.warn {
background: var(--app-warn-container);
color: var(--app-warn);
}
.notice.bad {
background: var(--app-bad-container);
color: var(--app-bad);
}
pre.code {
margin: 0;
padding: 12px 16px;
border-radius: var(--r-control);
background: var(--app-card-inset);
font-family: var(--font-mono);
font-size: 12.5px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
.spinner {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid var(--app-outline);
border-top-color: var(--app-accent);
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ── 图表 ── */
/* 拖动选区和十字光标:uPlot 自带的颜色在暗色下几乎看不见,换成主题令牌 */
.chart .u-select {
background: color-mix(in srgb, var(--app-accent), transparent 72%);
border-left: 1px solid var(--app-accent);
border-right: 1px solid var(--app-accent);
}
.chart .u-cursor-x,
.chart .u-cursor-y {
border-color: var(--app-ink-faint);
}
.chart-wrap {
position: relative;
}
.chart-hint {
margin-left: auto;
color: var(--app-ink-faint);
}
.chart-hint .btn {
height: 22px;
padding: 0 8px;
}
.chart-tip {
display: none;
position: absolute;
z-index: 5;
pointer-events: none;
padding: 6px 10px;
border-radius: 8px;
background: var(--app-primary);
color: var(--app-on-primary);
font-size: 12px;
line-height: 1.5;
white-space: pre;
font-variant-numeric: tabular-nums;
}
.group > button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.group > button:disabled:hover {
background: var(--app-card-inset);
}
.chart {
width: 100%;
}
.chart .u-legend {
font-family: var(--font-sans);
font-size: 12px;
color: var(--app-ink-muted);
}
.chart-legend {
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
font-size: 12px;
color: var(--app-ink-muted);
margin-bottom: 4px;
}
.chart-legend i {
display: inline-block;
width: 10px;
height: 3px;
border-radius: 2px;
margin-right: 6px;
vertical-align: middle;
}
/* ── 组件上的加载进度条:贴着卡片的上边缘,流动的渐变 ── */
.has-loading {
position: relative;
overflow: hidden;
}
.loading-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
overflow: hidden;
pointer-events: none;
}
.loading-bar i {
display: block;
width: 40%;
height: 100%;
border-radius: 3px;
background: var(--chart-1);
animation: loading-bar-slide 1.1s ease-in-out infinite;
}
@keyframes loading-bar-slide {
from {
transform: translateX(-100%);
}
to {
transform: translateX(250%);
}
}
/* 还没有数据时的占位 */
.skeleton {
position: relative;
overflow: hidden;
min-height: 72px;
border-radius: var(--r-card);
background: var(--app-card);
}
.skeleton.chart {
min-height: 260px;
}
.skeleton::after {
content: '';
position: absolute;
inset: 16px 16px auto;
height: 12px;
width: 40%;
border-radius: 6px;
background: var(--app-card-inset);
}
@media (prefers-reduced-motion: reduce) {
.loading-bar i {
animation: none;
width: 100%;
}
}
/* ── 带标签切换的命令框 ── */
.cmd-tabs {
border-radius: var(--r-control);
background: var(--app-card-inset);
overflow: hidden;
}
.cmd-tablist {
display: flex;
gap: 4px;
padding: 0 12px;
border-bottom: 1px solid var(--app-outline);
}
.cmd-tablist button {
position: relative;
padding: 10px 10px 9px;
border: 0;
background: transparent;
color: var(--app-ink-faint);
font: inherit;
font-size: 13px;
cursor: pointer;
}
.cmd-tablist button:hover {
color: var(--app-ink);
}
.cmd-tablist button.on {
color: var(--app-ink);
font-weight: 600;
}
.cmd-tablist button.on::after {
content: '';
position: absolute;
left: 10px;
right: 10px;
bottom: -1px;
height: 2px;
border-radius: 1px;
background: var(--app-ink);
}
.cmd-body {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 12px 12px 12px 14px;
}
.cmd-body code {
flex: 1;
min-width: 0;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 12.5px;
line-height: 1.6;
word-break: break-all;
white-space: pre-wrap; /* 容器命令是多行的(行尾反斜杠续行) */
user-select: all;
}
.cmd-copy {
flex: none;
display: inline-grid;
place-items: center;
width: 28px;
height: 28px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--app-ink-faint);
cursor: pointer;
}
.cmd-copy:hover {
background: var(--app-card-hover);
color: var(--app-ink);
}
/* ── 数据源列表:连接中的高亮,点一行就切换 ── */
.src-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.src-row {
display: flex;
align-items: center;
gap: 8px;
}
.src-pick {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--app-outline-soft);
border-radius: var(--r-control);
background: var(--app-card-inset);
color: var(--app-ink);
font: inherit;
text-align: left;
cursor: pointer;
}
.src-pick:hover {
background: var(--app-card-hover);
}
.src-row.on .src-pick,
.src-pick.on {
border-color: var(--app-accent);
background: var(--app-accent-container);
color: var(--app-on-accent-container);
}
.src-dot {
flex: none;
width: 10px;
height: 10px;
border-radius: 5px;
border: 2px solid var(--app-ink-faint);
}
.src-row.on .src-dot,
.src-pick.on .src-dot {
border-color: var(--app-accent);
background: var(--app-accent);
}
.src-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.src-text b {
font-weight: 600;
}
.src-text small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--app-ink-muted);
font-size: 12px;
}
/* ── 数据源状态条:放在用户信息上方,一眼看到连的是哪个后端、连得上没有 ── */
.src-status {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
padding: 8px 12px;
border: 1px solid var(--app-outline-soft);
border-radius: var(--r-control);
background: var(--app-card);
color: var(--app-ink);
font: inherit;
font-size: 12px;
text-align: left;
cursor: pointer;
}
.src-status:hover {
background: var(--app-card-hover);
}
.src-status .st-dot {
flex: none;
width: 8px;
height: 8px;
border-radius: 4px;
background: var(--app-idle);
}
.src-status.ok .st-dot {
background: var(--app-ok);
}
.src-status.bad .st-dot {
background: var(--app-bad);
}
.src-status .st-host {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
.src-status .st {
flex: none;
color: var(--app-ink-muted);
}
.src-status.bad .st {
color: var(--app-bad);
}
/* ── 节点的国旗、运营商图标、系统和架构 ── */
.node-tag {
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 0;
vertical-align: middle;
}
.node-tag .flag {
flex: none;
width: 18px;
height: 13.5px;
border-radius: 2px;
object-fit: cover;
box-shadow: 0 0 0 1px var(--app-outline-soft);
}
.node-tag .flag.ph {
display: inline-block;
background: var(--app-card-inset);
}
.node-tag .isp {
flex: none;
width: 15px;
height: 15px;
}
/* 三大运营商的标志:留出一点空,比例保持 */
.node-tag .isp.logo {
width: 17px;
height: 17px;
object-fit: contain;
}
.node-tag .isp rect {
fill: var(--app-ink-faint);
}
.node-tag .isp text {
fill: var(--isp-ink);
font-size: 10px;
font-weight: 700;
}
.node-tag .isp-mobile rect {
fill: var(--isp-mobile);
}
.node-tag .isp-telecom rect {
fill: var(--isp-telecom);
}
.node-tag .isp-unicom rect {
fill: var(--isp-unicom);
}
.node-tag .isp-broadnet rect {
fill: var(--isp-broadnet);
}
.node-tag .isp-name,
.node-tag .plat {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
color: var(--app-ink-faint);
}
/* 系统的小图标:有品牌色的用品牌色(和文字色混一点,亮暗两个主题里都看得清),没有的跟文字走 */
.os-icon {
flex: none;
width: 14px;
height: 14px;
}
.os-icon path.brand {
fill: color-mix(in srgb, var(--os-c) 72%, var(--app-ink));
}
.os-icon path.plain {
fill: currentColor;
}
.node-tag .plat {
display: inline-flex;
align-items: center;
gap: 4px;
}