mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(skill): detail drawer + Agent Tool Advanced fold
This commit is contained in:
parent
688b37b652
commit
169a09506b
@ -772,6 +772,8 @@ export default {
|
||||
binding: {
|
||||
skillsHint: 'Select skills this agent can use. Leave empty to use all enabled skills.',
|
||||
toolsHint: 'Select tools this agent can use. Leave empty to use all enabled tools.',
|
||||
advancedToolsTitle: 'Advanced: Hand-picked atomic tools',
|
||||
advancedToolsHint: 'Skill bindings already auto-expand allowed tools. Use this only for built-in micro-utilities not packaged as a skill (e.g. datetime, delegate_agent).',
|
||||
providersHint: 'Preferred provider order for this agent (lower index tried first). Leave empty to use the global available-pool order. Cooling-down or pool-removed providers are still skipped automatically.',
|
||||
providersAddHint: 'Click a provider below to add it to the preference list:',
|
||||
noSkills: 'No skills available',
|
||||
@ -1930,10 +1932,21 @@ export default {
|
||||
},
|
||||
actions: {
|
||||
configure: 'Configure',
|
||||
view: 'View',
|
||||
delete: 'Delete',
|
||||
saveChanges: 'Save Changes',
|
||||
createSkill: 'Create Skill',
|
||||
},
|
||||
detail: {
|
||||
title: 'Skill detail',
|
||||
manifest: 'Manifest',
|
||||
tools: 'Tools',
|
||||
features: 'Features',
|
||||
noManifest: 'This skill does not declare a v3 manifest. Legacy fields apply.',
|
||||
noTools: 'No tools advertised by active features.',
|
||||
noFeatures: 'No features[] matrix declared. The skill is treated as a single default feature.',
|
||||
toolsHint: 'These tool names are merged into the LLM allowed-tool list when this skill is bound to an agent. Tools owned by SETUP_NEEDED features stay hidden.',
|
||||
},
|
||||
runtime: {
|
||||
disabled: 'Disabled',
|
||||
unknown: 'Unknown',
|
||||
|
||||
@ -772,6 +772,8 @@ export default {
|
||||
binding: {
|
||||
skillsHint: '选择此智能体可使用的技能。留空则使用所有已启用的技能。',
|
||||
toolsHint: '选择此智能体可使用的工具。留空则使用所有已启用的工具。',
|
||||
advancedToolsTitle: '高级:手选原子工具',
|
||||
advancedToolsHint: 'Skill 绑定已自动展开 allowed-tools。此处仅用于未打包成 Skill 的内置微工具(如 datetime、delegate_agent)。',
|
||||
providersHint: '此智能体优先使用的提供商顺序(数字越小越先尝试)。留空则按全局可用池顺序回退。提供商进入冷却或被移出池时仍会被自动跳过。',
|
||||
providersAddHint: '点击下方提供商加入偏好列表:',
|
||||
noSkills: '暂无可用技能',
|
||||
@ -1932,10 +1934,21 @@ export default {
|
||||
},
|
||||
actions: {
|
||||
configure: '配置',
|
||||
view: '查看',
|
||||
delete: '删除',
|
||||
saveChanges: '保存更改',
|
||||
createSkill: '创建技能',
|
||||
},
|
||||
detail: {
|
||||
title: '技能详情',
|
||||
manifest: 'Manifest',
|
||||
tools: '工具',
|
||||
features: '特性',
|
||||
noManifest: '该技能未声明 v3 manifest,使用旧字段。',
|
||||
noTools: '当前激活特性未暴露任何工具。',
|
||||
noFeatures: '未声明 features[] 矩阵,技能被视为单一默认特性。',
|
||||
toolsHint: 'Skill 绑定到 agent 时,这些工具名将合并进 LLM 的 allowed-tools。SETUP_NEEDED 特性下的工具保持隐藏。',
|
||||
},
|
||||
runtime: {
|
||||
disabled: '已停用',
|
||||
unknown: '未知',
|
||||
|
||||
@ -233,6 +233,8 @@ export interface Skill {
|
||||
|
||||
/** 运行时解析状态(来自 /runtime/status) */
|
||||
export interface SkillRuntimeStatus {
|
||||
/** RFC-090 Phase 2 — entity primary key */
|
||||
id?: number
|
||||
name: string
|
||||
description?: string
|
||||
source: string // "directory" | "database"
|
||||
@ -256,6 +258,77 @@ export interface SkillRuntimeStatus {
|
||||
dependencySummary?: string | null
|
||||
// Computed label
|
||||
runtimeStatusLabel?: string
|
||||
// RFC-090 §14.1 — features matrix + manifest SoT
|
||||
manifest?: SkillManifest | null
|
||||
/** Map<featureId, "READY" | "SETUP_NEEDED" | "UNSUPPORTED"> */
|
||||
featureStatuses?: Record<string, string>
|
||||
/** featureIds whose status is READY */
|
||||
activeFeatures?: string[]
|
||||
/** Tools advertised to the LLM after feature filtering */
|
||||
effectiveAllowedTools?: string[]
|
||||
}
|
||||
|
||||
/** RFC-090 §14.6 — typed view onto manifest_json */
|
||||
export interface SkillManifest {
|
||||
id?: string
|
||||
name?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
version?: string
|
||||
author?: string
|
||||
/** prompt | code | mcp | acp | knowledge */
|
||||
type?: string
|
||||
category?: string
|
||||
allowedTools?: string[]
|
||||
requires?: SkillManifestRequirement[]
|
||||
platforms?: string[]
|
||||
features?: SkillManifestFeature[]
|
||||
settings?: SkillManifestSetting[]
|
||||
requiresModel?: string[]
|
||||
dashboardMetrics?: SkillManifestDashboardMetric[]
|
||||
selfEvolution?: { lessonsEnabled?: boolean; lessonsMaxEntries?: number; memoryWritesAllowed?: boolean }
|
||||
knowledge?: {
|
||||
bindKb?: string
|
||||
retrieval?: string
|
||||
topK?: number
|
||||
citation?: string
|
||||
rerank?: boolean
|
||||
boundKbId?: number | null
|
||||
} | null
|
||||
extras?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface SkillManifestRequirement {
|
||||
key: string
|
||||
type?: string
|
||||
check?: string
|
||||
optional?: boolean
|
||||
description?: string
|
||||
install?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface SkillManifestFeature {
|
||||
id: string
|
||||
label?: string
|
||||
requires?: string[]
|
||||
platforms?: string[]
|
||||
tools?: string[]
|
||||
fallbackMessage?: string
|
||||
unsupportedMessage?: string
|
||||
}
|
||||
|
||||
export interface SkillManifestSetting {
|
||||
key: string
|
||||
label?: string
|
||||
type?: string
|
||||
defaultValue?: any
|
||||
options?: Record<string, any>[]
|
||||
}
|
||||
|
||||
export interface SkillManifestDashboardMetric {
|
||||
label?: string
|
||||
memoryKey?: string
|
||||
format?: string
|
||||
}
|
||||
|
||||
/** 安全扫描发现 */
|
||||
|
||||
@ -243,26 +243,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools Tab -->
|
||||
<!-- Tools Tab — RFC-090 §9.2 调整 B: Advanced bypass for atomic
|
||||
tools not packaged as skills (e.g. datetime, delegate_agent).
|
||||
Skill bindings already auto-expand allowed-tools (§14.2), so
|
||||
the picker is collapsed by default to reduce noise. -->
|
||||
<div v-if="modalTab === 'tools'" class="binding-tab">
|
||||
<p class="binding-hint">{{ t('agents.binding.toolsHint') }}</p>
|
||||
<div v-if="availableTools.length === 0" class="binding-empty">{{ t('agents.binding.noTools') }}</div>
|
||||
<div v-else class="binding-list">
|
||||
<label
|
||||
v-for="tool in availableTools"
|
||||
:key="tool.name"
|
||||
class="binding-item"
|
||||
:class="{ selected: selectedToolNames.includes(tool.name) }"
|
||||
>
|
||||
<input type="checkbox" :value="tool.name" v-model="selectedToolNames" class="binding-checkbox" />
|
||||
<span class="binding-icon">{{ tool.icon || '🔧' }}</span>
|
||||
<div class="binding-info">
|
||||
<span class="binding-name">{{ tool.displayName || tool.name }}</span>
|
||||
<span v-if="tool.description" class="binding-desc">{{ tool.description?.slice(0, 80) }}</span>
|
||||
</div>
|
||||
<span class="binding-type-badge">{{ tool.toolType }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<details class="advanced-tools" :open="selectedToolNames.length > 0 || advancedToolsOpen">
|
||||
<summary class="advanced-tools-summary" @click.prevent="advancedToolsOpen = !advancedToolsOpen">
|
||||
<span class="advanced-tools-title">
|
||||
{{ t('agents.binding.advancedToolsTitle') }}
|
||||
<span v-if="selectedToolNames.length > 0" class="advanced-tools-count">{{ selectedToolNames.length }}</span>
|
||||
</span>
|
||||
<span class="advanced-tools-chevron">{{ (advancedToolsOpen || selectedToolNames.length > 0) ? '▾' : '▸' }}</span>
|
||||
</summary>
|
||||
<p class="binding-hint">{{ t('agents.binding.toolsHint') }}</p>
|
||||
<p class="binding-hint advanced-tools-note">{{ t('agents.binding.advancedToolsHint') }}</p>
|
||||
<div v-if="availableTools.length === 0" class="binding-empty">{{ t('agents.binding.noTools') }}</div>
|
||||
<div v-else class="binding-list">
|
||||
<label
|
||||
v-for="tool in availableTools"
|
||||
:key="tool.name"
|
||||
class="binding-item"
|
||||
:class="{ selected: selectedToolNames.includes(tool.name) }"
|
||||
>
|
||||
<input type="checkbox" :value="tool.name" v-model="selectedToolNames" class="binding-checkbox" />
|
||||
<span class="binding-icon">{{ tool.icon || '🔧' }}</span>
|
||||
<div class="binding-info">
|
||||
<span class="binding-name">{{ tool.displayName || tool.name }}</span>
|
||||
<span v-if="tool.description" class="binding-desc">{{ tool.description?.slice(0, 80) }}</span>
|
||||
</div>
|
||||
<span class="binding-type-badge">{{ tool.toolType }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Providers Tab (RFC-009 PR-3) -->
|
||||
@ -324,6 +337,10 @@ const activeFilter = ref('all')
|
||||
const showModal = ref(false)
|
||||
const editingAgent = ref<Agent | null>(null)
|
||||
const modalTab = ref<'basic' | 'skills' | 'tools' | 'providers'>('basic')
|
||||
/** RFC-090 §9.2 调整 B — Tool picker is an Advanced bypass; collapsed by
|
||||
* default but stays open as soon as the agent has any direct tool
|
||||
* bindings, so existing users don't lose visibility on their picks. */
|
||||
const advancedToolsOpen = ref(false)
|
||||
|
||||
// Binding state
|
||||
const availableSkills = ref<any[]>([])
|
||||
@ -868,4 +885,15 @@ async function toggleAgent(agent: Agent) {
|
||||
|
||||
.template-tags { display: flex; flex-wrap: wrap; gap: 4px; align-self: flex-start; margin-top: 2px; }
|
||||
.tag-chip { font-size: 11px; padding: 2px 8px; background: var(--mc-bg-sunken); color: var(--mc-text-tertiary); border-radius: 999px; white-space: nowrap; }
|
||||
|
||||
/* RFC-090 §9.2 调整 B — Advanced Tools picker (collapsed by default) */
|
||||
.advanced-tools { border: 1px dashed var(--mc-border); border-radius: 12px; padding: 0; }
|
||||
.advanced-tools[open] { padding: 12px 14px; }
|
||||
.advanced-tools-summary { list-style: none; cursor: pointer; padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; gap: 8px; user-select: none; color: var(--mc-text-secondary); font-size: 13px; font-weight: 600; }
|
||||
.advanced-tools-summary::-webkit-details-marker { display: none; }
|
||||
.advanced-tools[open] > .advanced-tools-summary { padding: 0 0 8px; border-bottom: 1px solid var(--mc-border-light); margin-bottom: 8px; }
|
||||
.advanced-tools-title { display: inline-flex; align-items: center; gap: 8px; }
|
||||
.advanced-tools-count { padding: 1px 8px; background: var(--mc-primary-bg); color: var(--mc-primary); border-radius: 999px; font-size: 11px; font-weight: 700; }
|
||||
.advanced-tools-chevron { color: var(--mc-text-tertiary); font-size: 12px; }
|
||||
.advanced-tools-note { font-style: italic; color: var(--mc-text-tertiary); margin-top: 4px; }
|
||||
</style>
|
||||
|
||||
@ -114,6 +114,10 @@
|
||||
<span v-if="getDependencyBadge(skill)" class="runtime-badge" :class="getDependencyBadge(skill)?.cls">
|
||||
{{ getDependencyBadge(skill)?.label }}
|
||||
</span>
|
||||
<!-- RFC-090 §14.1 features 矩阵:Setup Needed (M/N) -->
|
||||
<span v-if="getFeaturesBadge(skill)" class="runtime-badge" :class="getFeaturesBadge(skill)?.cls">
|
||||
{{ getFeaturesBadge(skill)?.label }}
|
||||
</span>
|
||||
<span v-if="getSourceBadge(skill)" class="source-badge">{{ getSourceBadge(skill) }}</span>
|
||||
<span v-if="getRuntimePath(skill)" class="skill-source-path">{{ getRuntimePath(skill) }}</span>
|
||||
</div>
|
||||
@ -179,6 +183,12 @@
|
||||
<div class="skill-footer">
|
||||
<span v-if="skill.author" class="skill-author">by {{ skill.author }}</span>
|
||||
<div class="skill-actions">
|
||||
<button class="skill-btn" @click="openDetailDrawer(skill)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/>
|
||||
</svg>
|
||||
{{ t('skills.actions.view') }}
|
||||
</button>
|
||||
<button class="skill-btn" @click="openEditModal(skill)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
@ -224,6 +234,72 @@
|
||||
<!-- Import Hub Dialog -->
|
||||
<ImportHubDialog v-model:visible="showImportDialog" @installed="loadAll" />
|
||||
|
||||
<!-- RFC-090 Phase 3 — Skill detail drawer (Manifest / Tools / Features) -->
|
||||
<el-drawer
|
||||
v-model="detailDrawerVisible"
|
||||
:title="detailSkill ? resolveSkillName(detailSkill) : t('skills.detail.title')"
|
||||
direction="rtl"
|
||||
size="640px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<div v-if="detailSkill" class="detail-drawer">
|
||||
<div class="detail-tabs">
|
||||
<button class="detail-tab" :class="{ active: detailTab === 'manifest' }" @click="detailTab = 'manifest'">
|
||||
{{ t('skills.detail.manifest') }}
|
||||
</button>
|
||||
<button class="detail-tab" :class="{ active: detailTab === 'tools' }" @click="detailTab = 'tools'">
|
||||
{{ t('skills.detail.tools') }}
|
||||
<span v-if="detailToolsCount > 0" class="tab-count">{{ detailToolsCount }}</span>
|
||||
</button>
|
||||
<button class="detail-tab" :class="{ active: detailTab === 'features' }" @click="detailTab = 'features'">
|
||||
{{ t('skills.detail.features') }}
|
||||
<span v-if="detailFeaturesCount > 0" class="tab-count">{{ detailFeaturesCount }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Manifest tab -->
|
||||
<div v-if="detailTab === 'manifest'" class="detail-section">
|
||||
<p v-if="!detailManifest" class="detail-empty">{{ t('skills.detail.noManifest') }}</p>
|
||||
<pre v-else class="detail-pre">{{ detailManifestPretty }}</pre>
|
||||
</div>
|
||||
<!-- Tools tab -->
|
||||
<div v-if="detailTab === 'tools'" class="detail-section">
|
||||
<p v-if="detailToolsCount === 0" class="detail-empty">{{ t('skills.detail.noTools') }}</p>
|
||||
<ul v-else class="detail-tool-list">
|
||||
<li v-for="tool in detailEffectiveTools" :key="tool" class="detail-tool-item">
|
||||
<code>{{ tool }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="detail-hint">{{ t('skills.detail.toolsHint') }}</p>
|
||||
</div>
|
||||
<!-- Features tab -->
|
||||
<div v-if="detailTab === 'features'" class="detail-section">
|
||||
<p v-if="detailFeaturesCount === 0" class="detail-empty">{{ t('skills.detail.noFeatures') }}</p>
|
||||
<ul v-else class="detail-feature-list">
|
||||
<li v-for="feat in detailFeatures" :key="feat.id" class="detail-feature-item">
|
||||
<div class="detail-feature-head">
|
||||
<span class="detail-feature-id">{{ feat.id }}</span>
|
||||
<span class="detail-feature-status" :class="`feat-${(feat.status || 'unknown').toLowerCase()}`">
|
||||
{{ feat.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="feat.label" class="detail-feature-label">{{ feat.label }}</div>
|
||||
<div v-if="feat.requires?.length" class="detail-feature-meta">
|
||||
<span class="detail-meta-key">requires:</span>
|
||||
<span v-for="r in feat.requires" :key="r" class="detail-feature-tag">{{ r }}</span>
|
||||
</div>
|
||||
<div v-if="feat.platforms?.length" class="detail-feature-meta">
|
||||
<span class="detail-meta-key">platforms:</span>
|
||||
<span v-for="p in feat.platforms" :key="p" class="detail-feature-tag">{{ p }}</span>
|
||||
</div>
|
||||
<div v-if="feat.fallbackMessage" class="detail-feature-fallback">
|
||||
{{ feat.fallbackMessage }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- Modal -->
|
||||
<div v-if="showModal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
@ -350,6 +426,34 @@ const query = reactive({
|
||||
const expandedFindings = ref<Record<string, boolean>>({})
|
||||
const rescanning = ref<Record<string, boolean>>({})
|
||||
|
||||
/** RFC-090 Phase 3 — detail drawer state. */
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detailSkill = ref<Skill | null>(null)
|
||||
const detailTab = ref<'manifest' | 'tools' | 'features'>('manifest')
|
||||
|
||||
const detailRuntime = computed(() =>
|
||||
detailSkill.value ? runtimeStatusMap.value[detailSkill.value.name] || null : null,
|
||||
)
|
||||
const detailManifest = computed(() => detailRuntime.value?.manifest ?? null)
|
||||
const detailManifestPretty = computed(() =>
|
||||
detailManifest.value ? JSON.stringify(detailManifest.value, null, 2) : '',
|
||||
)
|
||||
const detailEffectiveTools = computed(() => detailRuntime.value?.effectiveAllowedTools || [])
|
||||
const detailToolsCount = computed(() => detailEffectiveTools.value.length)
|
||||
const detailFeatures = computed(() => {
|
||||
const m = detailManifest.value
|
||||
if (!m || !m.features) return []
|
||||
const statuses = detailRuntime.value?.featureStatuses || {}
|
||||
return m.features.map(f => ({ ...f, status: statuses[f.id] || 'UNKNOWN' }))
|
||||
})
|
||||
const detailFeaturesCount = computed(() => detailFeatures.value.length)
|
||||
|
||||
function openDetailDrawer(skill: Skill) {
|
||||
detailSkill.value = skill
|
||||
detailTab.value = 'manifest'
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
|
||||
const categoryTabs = computed(() => [
|
||||
{ label: t('skills.tabs.all'), value: 'all', icon: '🗂️' },
|
||||
{ label: t('skills.tabs.builtin'), value: 'builtin', icon: '🔧' },
|
||||
@ -691,6 +795,37 @@ function getDependencyBadge(skill: Skill): { label: string; cls: string } | null
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-090 §14.1 — when a manifest declares a features[] matrix, surface
|
||||
* "Setup Needed (M/N)" so the user sees partial readiness instead of a
|
||||
* binary "all-or-nothing" deps badge. Returns null when the manifest has
|
||||
* no explicit features (legacy skills or single-feature shape) — the
|
||||
* existing dependency badge already covers that case.
|
||||
*/
|
||||
function getFeaturesBadge(skill: Skill): { label: string; cls: string } | null {
|
||||
if (!skill.enabled) return null
|
||||
const rt = getRuntimeStatus(skill)
|
||||
if (!rt || !rt.manifest) return null
|
||||
const declaredFeatures = rt.manifest.features || []
|
||||
if (declaredFeatures.length === 0) return null
|
||||
const statuses = rt.featureStatuses || {}
|
||||
const total = declaredFeatures.length
|
||||
const ready = (rt.activeFeatures || []).length
|
||||
if (ready === total) {
|
||||
return { label: `${ready}/${total} ready`, cls: 'rt-features-ready' }
|
||||
}
|
||||
if (ready === 0) {
|
||||
return { label: `Setup Needed (0/${total})`, cls: 'rt-deps-missing' }
|
||||
}
|
||||
// Mixed: at least one feature is not READY.
|
||||
// Distinguish UNSUPPORTED vs SETUP_NEEDED to color appropriately.
|
||||
const anyUnsupported = Object.values(statuses).some(s => s === 'UNSUPPORTED')
|
||||
return {
|
||||
label: `Setup Needed (${ready}/${total})`,
|
||||
cls: anyUnsupported ? 'rt-features-mixed' : 'rt-deps-missing',
|
||||
}
|
||||
}
|
||||
|
||||
function getMissingDeps(skill: Skill): string[] {
|
||||
const rt = getRuntimeStatus(skill)
|
||||
return rt?.missingDependencies || []
|
||||
@ -997,6 +1132,11 @@ html.dark .scan-finding-item { background: rgba(255, 255, 255, 0.05); }
|
||||
:root.dark .rt-synthesized { background: rgba(99, 102, 241, 0.15); color: #818cf8; }
|
||||
.rt-sec-warning { background: var(--mc-primary-bg); color: var(--mc-primary-hover); }
|
||||
.rt-deps-missing { background: var(--mc-primary-bg); color: var(--mc-primary-hover); }
|
||||
/* RFC-090 §14.1 — features 矩阵徽标 */
|
||||
.rt-features-ready { background: rgba(34, 197, 94, 0.12); color: #16a34a; }
|
||||
.rt-features-mixed { background: rgba(99, 102, 241, 0.12); color: #6366f1; }
|
||||
:root.dark .rt-features-ready { background: rgba(34, 197, 94, 0.2); color: #4ade80; }
|
||||
:root.dark .rt-features-mixed { background: rgba(129, 140, 248, 0.18); color: #a5b4fc; }
|
||||
.rt-disabled { background: var(--mc-bg-sunken); color: var(--mc-text-tertiary); }
|
||||
.rt-unknown { background: var(--mc-bg-sunken); color: var(--mc-text-tertiary); }
|
||||
.skill-source-path { font-size: 11px; color: var(--mc-text-tertiary); font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; }
|
||||
@ -1047,4 +1187,34 @@ html.dark .scan-finding-item { background: rgba(255, 255, 255, 0.05); }
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* RFC-090 Phase 3 — detail drawer */
|
||||
.detail-drawer { padding: 0 16px 16px; display: flex; flex-direction: column; gap: 16px; }
|
||||
.detail-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--mc-border-light); padding-bottom: 4px; }
|
||||
.detail-tab { padding: 8px 14px; border: none; background: none; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--mc-text-secondary); border-radius: 8px 8px 0 0; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.detail-tab:hover { color: var(--mc-text-primary); background: var(--mc-bg-muted); }
|
||||
.detail-tab.active { color: var(--mc-primary); background: var(--mc-primary-bg); border-bottom: 2px solid var(--mc-primary); margin-bottom: -1px; font-weight: 600; }
|
||||
.tab-count { font-size: 10px; padding: 1px 6px; border-radius: 10px; background: var(--mc-bg-sunken); color: var(--mc-text-tertiary); font-weight: 600; }
|
||||
.detail-tab.active .tab-count { background: var(--mc-primary); color: white; }
|
||||
.detail-section { padding: 4px 0; }
|
||||
.detail-empty { color: var(--mc-text-tertiary); font-size: 13px; font-style: italic; }
|
||||
.detail-pre { background: var(--mc-bg-sunken); padding: 12px; border-radius: 8px; max-height: 480px; overflow: auto; font-size: 12px; line-height: 1.5; color: var(--mc-text-primary); font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; white-space: pre-wrap; word-break: break-word; }
|
||||
.detail-tool-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.detail-tool-item code { display: block; padding: 6px 10px; background: var(--mc-bg-sunken); border-radius: 8px; font-size: 12px; color: var(--mc-text-primary); }
|
||||
.detail-hint { margin-top: 12px; font-size: 12px; color: var(--mc-text-tertiary); line-height: 1.5; }
|
||||
.detail-feature-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.detail-feature-item { padding: 12px; border: 1px solid var(--mc-border-light); border-radius: 12px; background: var(--mc-bg-muted); }
|
||||
.detail-feature-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.detail-feature-id { font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px; font-weight: 600; color: var(--mc-text-primary); }
|
||||
.detail-feature-status { font-size: 10px; padding: 2px 8px; border-radius: 999px; font-weight: 700; letter-spacing: 0.04em; }
|
||||
.feat-ready { background: rgba(34, 197, 94, 0.12); color: #16a34a; }
|
||||
.feat-setup_needed { background: var(--mc-primary-bg); color: var(--mc-primary-hover); }
|
||||
.feat-unsupported { background: var(--mc-bg-sunken); color: var(--mc-text-tertiary); }
|
||||
.feat-unknown { background: var(--mc-bg-sunken); color: var(--mc-text-tertiary); }
|
||||
:root.dark .feat-ready { background: rgba(34, 197, 94, 0.2); color: #4ade80; }
|
||||
.detail-feature-label { font-size: 13px; color: var(--mc-text-secondary); margin: 4px 0 6px; }
|
||||
.detail-feature-meta { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin: 4px 0; font-size: 11px; }
|
||||
.detail-meta-key { color: var(--mc-text-tertiary); margin-right: 4px; }
|
||||
.detail-feature-tag { padding: 2px 6px; background: var(--mc-bg-elevated); border-radius: 4px; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; color: var(--mc-text-primary); }
|
||||
.detail-feature-fallback { font-size: 11px; color: var(--mc-primary-hover); margin-top: 6px; font-style: italic; }
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user