本地开发换成真订阅;闪电挪到卡片上;忙碌状态改成按卡片
占位线路删了。它们指向 127.0.0.1:1080,从来没测通过,而 URLTest 只在你选的那条 「对测试地址是活的」时才认账(urltest.go:106-111)——所以点了立刻被切回去, 一个正常行为看起来像 bug。现在本地内核直接用真订阅,通过隧道拉。 订阅地址带访问令牌,不能进 git:dev/config.yaml 变成模板,dev:reset 从 .env 取值生成 dev/home/config.yaml。顺带确认内核认 base64 分享链接格式(provider.go:384-390 回退到 ConvertsV2Ray),所以 Sub-Store 的输出直接能用。 闪电从组标题挪到每张卡的左下角,也就是延迟显示的位置:测过显示毫秒数,没测过显示闪电, 两种都能点,点了测这一条。忙碌状态从「禁用整个界面」改成只有正在提交的那张卡显示虚线边框。
This commit is contained in:
+156
-325
@@ -1,26 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 选中那一路入站的详情。
|
||||
*
|
||||
* 分两块,因为它们是两种不同性质的动作,不该长得一样也不该给一样的反馈:
|
||||
* 流量去向 —— 换策略组当前选中谁。不写配置文件,点了立刻变。日常点的就是这个。
|
||||
* 出站 —— 换这路入站指向哪个出站。要改配置文件,走校验和重载,要等几秒。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
||||
import { latestDelay } from '../api'
|
||||
import type { ProxyInfo } from '../api'
|
||||
import OutboundCard from '../components/OutboundCard.vue'
|
||||
import IconOffline from '~icons/ri/cloud-off-line'
|
||||
import IconEdit from '~icons/ri/edit-line'
|
||||
import IconDelete from '~icons/ri/delete-bin-line'
|
||||
import IconFlash from '~icons/ri/flashlight-line'
|
||||
|
||||
const props = defineProps<{
|
||||
inbound: Inbound
|
||||
groups: string[]
|
||||
ownProxies: string[]
|
||||
providerLines: string[]
|
||||
runtime: Record<string, ProxyInfo>
|
||||
/** 流量最终交给哪个出站。走规则的入站要查到规则表末尾的 MATCH */
|
||||
destination: string | null
|
||||
/** 运行时状态,键是出站名字。内核没连接时是空的 */
|
||||
runtime: Record<string, ProxyInfo>
|
||||
kernelOnline: boolean
|
||||
saving: boolean
|
||||
/** 正在测延迟 */
|
||||
testing: boolean
|
||||
/** 正在提交的那个出站的名字。只有它显示忙碌,其余照常能点 */
|
||||
pendingOutbound: string | null
|
||||
/** 正在测延迟的线路 */
|
||||
testing: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -28,26 +35,36 @@ const emit = defineEmits<{
|
||||
edit: []
|
||||
remove: []
|
||||
select: [group: string, member: string]
|
||||
test: [group: string]
|
||||
test: [name: string]
|
||||
}>()
|
||||
|
||||
/** 默认入站没有 proxy 字段可写,「出站」那一块对它是只读的。 */
|
||||
const locked = computed(() => props.inbound.isDefault)
|
||||
|
||||
function describe(name: string): string {
|
||||
const info = props.runtime[name]
|
||||
if (!info) return ''
|
||||
return info.now ? `${info.type}(${info.now})` : info.type
|
||||
}
|
||||
|
||||
const delayOf = (name: string) => latestDelay(props.runtime[name])
|
||||
const isTesting = (name: string) => props.testing.includes(name)
|
||||
|
||||
/**
|
||||
* 从落脚的出站一路跟着「当前选中」往下走,直到走到一条真正的线路。
|
||||
*
|
||||
* 策略组可以选中另一个策略组,所以「这个端口现在走哪条线」往往要跳好几层:
|
||||
* PROXY 选了 AUTO,AUTO 选了某条线路。只显示第一层等于没回答问题。
|
||||
* 策略组可以选中另一个策略组,所以「这个端口现在走哪条线」往往要跳好几层。
|
||||
* 只显示第一层等于没回答问题。
|
||||
*/
|
||||
function resolveChain(start: string | null): string[] {
|
||||
const chain: string[] = []
|
||||
let cursor = start
|
||||
while (cursor && !chain.includes(cursor)) {
|
||||
chain.push(cursor)
|
||||
const chain = computed(() => {
|
||||
const hops: string[] = []
|
||||
let cursor = props.destination
|
||||
while (cursor && !hops.includes(cursor)) {
|
||||
hops.push(cursor)
|
||||
cursor = props.runtime[cursor]?.now ?? null
|
||||
}
|
||||
return chain
|
||||
}
|
||||
|
||||
const chain = computed(() => resolveChain(props.destination))
|
||||
return hops
|
||||
})
|
||||
|
||||
/** 链条上最后一个还是策略组的环节——用户能在这里换选中。 */
|
||||
const switchable = computed(() => {
|
||||
@@ -59,89 +76,20 @@ const switchable = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
interface Option {
|
||||
key: string
|
||||
mode: OutboundMode
|
||||
label: string
|
||||
/** 卡片第二行。策略组显示「类型(当前选中)」,线路显示协议 */
|
||||
sub: string
|
||||
delay: number | null
|
||||
disabled: boolean
|
||||
/** 不可选时的原因,鼠标悬停显示 */
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/** 默认入站没有 proxy 字段可写,整块都不可选。 */
|
||||
const locked = computed(() => props.inbound.isDefault)
|
||||
|
||||
function describe(name: string): { sub: string; delay: number | null } {
|
||||
const info = props.runtime[name]
|
||||
if (!info) return { sub: '', delay: null }
|
||||
const sub = info.now ? `${info.type}(${info.now})` : info.type
|
||||
return { sub, delay: latestDelay(info) }
|
||||
}
|
||||
|
||||
const builtins = computed<Option[]>(() =>
|
||||
(
|
||||
[
|
||||
{ mode: { kind: 'rules' } as OutboundMode, label: 'RULE', sub: '走分流规则' },
|
||||
{ mode: { kind: 'direct' } as OutboundMode, label: 'DIRECT', sub: '不经过代理' },
|
||||
{ mode: { kind: 'global' } as OutboundMode, label: 'GLOBAL', sub: '跟随全局选择' },
|
||||
] as const
|
||||
).map((b) => ({
|
||||
key: b.label,
|
||||
mode: b.mode,
|
||||
label: b.label,
|
||||
sub: b.sub,
|
||||
delay: null,
|
||||
disabled: locked.value,
|
||||
})),
|
||||
)
|
||||
|
||||
function named(names: string[]): Option[] {
|
||||
return names.map((name) => {
|
||||
const { sub, delay } = describe(name)
|
||||
return {
|
||||
key: name,
|
||||
mode: { kind: 'named', name } as OutboundMode,
|
||||
label: name,
|
||||
sub,
|
||||
delay,
|
||||
disabled: locked.value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const groupOptions = computed(() => named(props.groups))
|
||||
const ownOptions = computed(() => named(props.ownProxies))
|
||||
|
||||
const lineOptions = computed<Option[]>(() =>
|
||||
props.providerLines.map((name) => {
|
||||
const { sub, delay } = describe(name)
|
||||
return {
|
||||
key: name,
|
||||
mode: { kind: 'named', name } as OutboundMode,
|
||||
label: name,
|
||||
sub,
|
||||
delay,
|
||||
disabled: true,
|
||||
reason: '订阅线路不能被直接引用,选中它需要自动建一个筛选组——这部分还没做',
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
function isCurrent(mode: OutboundMode): boolean {
|
||||
function isCurrentOutbound(mode: OutboundMode): boolean {
|
||||
const now = props.inbound.outbound
|
||||
if (now.kind !== mode.kind) return false
|
||||
return now.kind !== 'named' || now.name === (mode as { name: string }).name
|
||||
}
|
||||
|
||||
function delayClass(ms: number | null): string {
|
||||
if (ms === null) return 'd-none'
|
||||
if (ms < 200) return 'd-ok'
|
||||
if (ms < 500) return 'd-warn'
|
||||
return 'd-bad'
|
||||
}
|
||||
const BUILTINS: Array<{ mode: OutboundMode; label: string; sub: string }> = [
|
||||
{ mode: { kind: 'rules' }, label: 'RULE', sub: '走分流规则' },
|
||||
{ mode: { kind: 'direct' }, label: 'DIRECT', sub: '不经过代理' },
|
||||
{ mode: { kind: 'global' }, label: 'GLOBAL', sub: '跟随全局选择' },
|
||||
]
|
||||
|
||||
const LINE_LOCKED = '订阅线路不能被入站直接引用,选中它需要自动建一个筛选组——这部分还没做'
|
||||
const NO_PROXY_FIELD = '这路入站没有 proxy 字段'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -151,11 +99,10 @@ function delayClass(ms: number | null): string {
|
||||
<span class="port mono">{{ inbound.port }}</span>
|
||||
<span class="name">{{ inbound.name }}</span>
|
||||
</div>
|
||||
<div v-if="!inbound.isDefault" class="acts">
|
||||
<button type="button" :disabled="saving" @click="emit('edit')"><IconEdit /> 编辑</button>
|
||||
<button type="button" class="danger" :disabled="saving" @click="emit('remove')">
|
||||
<IconDelete /> 删除
|
||||
</button>
|
||||
|
||||
<div v-if="!locked" class="acts">
|
||||
<button type="button" @click="emit('edit')"><IconEdit /> 编辑</button>
|
||||
<button type="button" class="danger" @click="emit('remove')"><IconDelete /> 删除</button>
|
||||
</div>
|
||||
|
||||
<dl class="facts">
|
||||
@@ -168,128 +115,114 @@ function delayClass(ms: number | null): string {
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
<section v-if="chain.length" class="pick">
|
||||
<!-- 运行时:换线路。不写文件,点了立刻变 -->
|
||||
<section v-if="chain.length" class="block">
|
||||
<h3>流量去向</h3>
|
||||
<p class="chain mono">
|
||||
<template v-for="(hop, i) in chain" :key="hop">
|
||||
<span v-if="i" class="arrow">→</span><span :class="i === chain.length - 1 && 'tail'">{{ hop }}</span>
|
||||
<span v-if="i" class="arrow">→</span>
|
||||
<span :class="i === chain.length - 1 && 'tail'">{{ hop }}</span>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<template v-if="switchable">
|
||||
<div class="sub-head">
|
||||
<h4>{{ switchable.group }} 当前选中</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="flash"
|
||||
:disabled="testing"
|
||||
:title="`测试 ${switchable.group} 里所有线路的延迟`"
|
||||
@click="emit('test', switchable.group)"
|
||||
>
|
||||
<IconFlash />
|
||||
</button>
|
||||
</div>
|
||||
<h4>{{ switchable.group }} 当前选中</h4>
|
||||
<div class="grid">
|
||||
<button
|
||||
<OutboundCard
|
||||
v-for="m in switchable.members"
|
||||
:key="m"
|
||||
type="button"
|
||||
:class="['card', m === switchable.now && 'is-on']"
|
||||
:disabled="saving"
|
||||
@click="emit('select', switchable.group, m)"
|
||||
>
|
||||
<span class="card-name">{{ m }}</span>
|
||||
<span class="card-sub">{{ runtime[m]?.now ? `${runtime[m]!.type}(${runtime[m]!.now})` : (runtime[m]?.type ?? '—') }}</span>
|
||||
<span :class="['card-delay', delayClass(latestDelay(runtime[m]))]">
|
||||
{{ latestDelay(runtime[m]) === null ? '' : `${latestDelay(runtime[m])} ms` }}
|
||||
</span>
|
||||
</button>
|
||||
:name="m"
|
||||
:sub="describe(m)"
|
||||
:delay="delayOf(m)"
|
||||
:selected="m === switchable.now"
|
||||
:testing="isTesting(m)"
|
||||
testable
|
||||
@pick="emit('select', switchable.group, m)"
|
||||
@test="emit('test', m)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section class="pick">
|
||||
<!-- 配置:换这路入站指向谁。要写文件、要校验 -->
|
||||
<section class="block">
|
||||
<h3>出站</h3>
|
||||
|
||||
<p v-if="locked" class="aside-note">
|
||||
由 <code>{{ inbound.legacyKey }}</code> 声明,这种写法没有 proxy 字段,出站固定是 RULE
|
||||
<p v-if="locked" class="hint">
|
||||
由 <code>{{ inbound.legacyKey }}</code> 声明,这种写法没有 proxy 字段,固定走 RULE
|
||||
</p>
|
||||
|
||||
<div v-if="!locked" class="grid">
|
||||
<button
|
||||
v-for="opt in builtins"
|
||||
:key="opt.key"
|
||||
type="button"
|
||||
:class="['card', isCurrent(opt.mode) && 'is-on']"
|
||||
:disabled="opt.disabled || saving"
|
||||
@click="emit('pick', opt.mode)"
|
||||
>
|
||||
<span class="card-name">{{ opt.label }}</span>
|
||||
<span class="card-sub">{{ opt.sub }}</span>
|
||||
</button>
|
||||
<OutboundCard
|
||||
v-for="b in BUILTINS"
|
||||
:key="b.label"
|
||||
:name="b.label"
|
||||
:sub="b.sub"
|
||||
:delay="null"
|
||||
:selected="isCurrentOutbound(b.mode)"
|
||||
:pending="pendingOutbound === b.label"
|
||||
@pick="emit('pick', b.mode)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="groupOptions.length">
|
||||
<template v-if="groups.length">
|
||||
<h4 :class="locked && 'is-dim'">策略组</h4>
|
||||
<div class="grid">
|
||||
<button
|
||||
v-for="opt in groupOptions"
|
||||
:key="opt.key"
|
||||
type="button"
|
||||
:class="['card', isCurrent(opt.mode) && 'is-on']"
|
||||
:disabled="opt.disabled || saving"
|
||||
@click="emit('pick', opt.mode)"
|
||||
>
|
||||
<span class="card-name">{{ opt.label }}</span>
|
||||
<span class="card-sub">{{ opt.sub || '—' }}</span>
|
||||
<span :class="['card-delay', delayClass(opt.delay)]">
|
||||
{{ opt.delay === null ? '' : `${opt.delay} ms` }}
|
||||
</span>
|
||||
</button>
|
||||
<OutboundCard
|
||||
v-for="g in groups"
|
||||
:key="g"
|
||||
:name="g"
|
||||
:sub="describe(g)"
|
||||
:delay="delayOf(g)"
|
||||
:selected="isCurrentOutbound({ kind: 'named', name: g })"
|
||||
:disabled-reason="locked ? NO_PROXY_FIELD : undefined"
|
||||
:pending="pendingOutbound === g"
|
||||
:testing="isTesting(g)"
|
||||
testable
|
||||
@pick="emit('pick', { kind: 'named', name: g })"
|
||||
@test="emit('test', g)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="ownOptions.length">
|
||||
<template v-if="ownProxies.length">
|
||||
<h4 :class="locked && 'is-dim'">自有线路</h4>
|
||||
<div class="grid">
|
||||
<button
|
||||
v-for="opt in ownOptions"
|
||||
:key="opt.key"
|
||||
type="button"
|
||||
:class="['card', isCurrent(opt.mode) && 'is-on']"
|
||||
:disabled="opt.disabled || saving"
|
||||
@click="emit('pick', opt.mode)"
|
||||
>
|
||||
<span class="card-name">{{ opt.label }}</span>
|
||||
<span class="card-sub">{{ opt.sub || '—' }}</span>
|
||||
<span :class="['card-delay', delayClass(opt.delay)]">
|
||||
{{ opt.delay === null ? '' : `${opt.delay} ms` }}
|
||||
</span>
|
||||
</button>
|
||||
<OutboundCard
|
||||
v-for="p in ownProxies"
|
||||
:key="p"
|
||||
:name="p"
|
||||
:sub="describe(p)"
|
||||
:delay="delayOf(p)"
|
||||
:selected="isCurrentOutbound({ kind: 'named', name: p })"
|
||||
:disabled-reason="locked ? NO_PROXY_FIELD : undefined"
|
||||
:pending="pendingOutbound === p"
|
||||
:testing="isTesting(p)"
|
||||
testable
|
||||
@pick="emit('pick', { kind: 'named', name: p })"
|
||||
@test="emit('test', p)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h4 :class="locked && 'is-dim'">订阅线路</h4>
|
||||
<p v-if="!kernelOnline" class="aside-note">
|
||||
<p v-if="!kernelOnline" class="hint">
|
||||
<IconOffline />
|
||||
内核未连接。订阅里有哪些线路只有运行中的内核知道
|
||||
内核未连接,看不到订阅里有哪些线路
|
||||
</p>
|
||||
<p v-else-if="lineOptions.length === 0" class="aside-note">订阅里没有线路</p>
|
||||
<p v-else-if="providerLines.length === 0" class="hint">订阅里没有线路</p>
|
||||
<div v-else class="grid">
|
||||
<button
|
||||
v-for="opt in lineOptions"
|
||||
:key="opt.key"
|
||||
type="button"
|
||||
class="card"
|
||||
disabled
|
||||
:title="opt.reason"
|
||||
>
|
||||
<span class="card-name">{{ opt.label }}</span>
|
||||
<span class="card-sub">{{ opt.sub || '—' }}</span>
|
||||
<span :class="['card-delay', delayClass(opt.delay)]">
|
||||
{{ opt.delay === null ? '' : `${opt.delay} ms` }}
|
||||
</span>
|
||||
</button>
|
||||
<OutboundCard
|
||||
v-for="line in providerLines"
|
||||
:key="line"
|
||||
:name="line"
|
||||
:sub="describe(line)"
|
||||
:delay="delayOf(line)"
|
||||
:selected="false"
|
||||
:disabled-reason="LINE_LOCKED"
|
||||
:testing="isTesting(line)"
|
||||
testable
|
||||
@test="emit('test', line)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
@@ -331,22 +264,6 @@ function delayClass(ms: number | null): string {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.facts {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--s-1) var(--s-4);
|
||||
margin: 0;
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.facts dt {
|
||||
color: var(--c-text-faint);
|
||||
}
|
||||
|
||||
.facts dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.acts {
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
@@ -365,63 +282,55 @@ function delayClass(ms: number | null): string {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.acts button:hover:not(:disabled) {
|
||||
.acts button:hover {
|
||||
border-color: var(--c-accent);
|
||||
color: var(--c-accent-text);
|
||||
}
|
||||
|
||||
.acts button.danger:hover:not(:disabled) {
|
||||
.acts button.danger:hover {
|
||||
border-color: var(--c-danger);
|
||||
color: var(--c-danger);
|
||||
}
|
||||
|
||||
.acts button:disabled {
|
||||
color: var(--c-text-faint);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.is-dim {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sub-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.flash {
|
||||
.facts {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-sm);
|
||||
background: none;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--s-1) var(--s-4);
|
||||
margin: 0;
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.facts dt {
|
||||
color: var(--c-text-faint);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flash:hover:not(:disabled) {
|
||||
background: var(--c-accent-soft);
|
||||
color: var(--c-accent-text);
|
||||
.facts dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flash:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pick {
|
||||
.block {
|
||||
padding-top: var(--s-4);
|
||||
}
|
||||
|
||||
.pick + .pick {
|
||||
.block + .block {
|
||||
margin-top: var(--s-5);
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--t-sm);
|
||||
font-weight: 600;
|
||||
color: var(--c-text-soft);
|
||||
margin-bottom: var(--s-3);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--t-xs);
|
||||
font-weight: 500;
|
||||
color: var(--c-text-faint);
|
||||
margin: var(--s-5) 0 var(--s-2);
|
||||
}
|
||||
|
||||
.chain {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -441,104 +350,26 @@ function delayClass(ms: number | null): string {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--t-sm);
|
||||
font-weight: 600;
|
||||
color: var(--c-text-soft);
|
||||
margin-bottom: var(--s-3);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--t-xs);
|
||||
font-weight: 500;
|
||||
color: var(--c-text-faint);
|
||||
margin: var(--s-5) 0 var(--s-2);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
min-width: 0;
|
||||
padding: var(--s-3);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--c-surface-sunken);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover:not(:disabled) {
|
||||
border-color: var(--c-accent);
|
||||
}
|
||||
|
||||
.card.is-on {
|
||||
border-color: var(--c-accent);
|
||||
background: var(--c-accent-soft);
|
||||
}
|
||||
|
||||
.card:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: var(--t-sm);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card.is-on .card-name {
|
||||
color: var(--c-accent-text);
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: var(--t-xs);
|
||||
color: var(--c-text-faint);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-delay {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--t-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.d-ok {
|
||||
color: var(--c-ok);
|
||||
}
|
||||
.d-warn {
|
||||
color: var(--c-warn);
|
||||
}
|
||||
.d-bad {
|
||||
color: var(--c-danger);
|
||||
}
|
||||
.d-none {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.aside-note {
|
||||
.hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
color: var(--c-text-faint);
|
||||
font-size: var(--t-sm);
|
||||
margin-bottom: var(--s-3);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.aside-note code {
|
||||
.hint code {
|
||||
color: var(--c-text-soft);
|
||||
}
|
||||
|
||||
.is-dim {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user