mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
refactor(ui): share drawer chrome via MateDrawer, migrate Doctor and skill drawers
This commit is contained in:
parent
196ddb8588
commit
15ee205ab9
276
mateclaw-ui/src/components/common/MateDrawer.vue
Normal file
276
mateclaw-ui/src/components/common/MateDrawer.vue
Normal file
@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="mc-drawer-fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="mc-drawer-overlay"
|
||||
:class="overlayClass"
|
||||
@click.self="emit('close')"
|
||||
>
|
||||
<div class="mc-drawer-panel" :class="panelClass">
|
||||
<div class="mc-drawer-header">
|
||||
<div class="mc-drawer-header__meta">
|
||||
<span v-if="$slots.icon" class="mc-drawer-icon-shell">
|
||||
<slot name="icon" />
|
||||
</span>
|
||||
<div class="mc-drawer-titles">
|
||||
<h3 class="mc-drawer-title">{{ title }}</h3>
|
||||
<p
|
||||
v-if="subtitle || $slots.subtitle"
|
||||
class="mc-drawer-subtitle"
|
||||
>
|
||||
<slot name="subtitle">{{ subtitle }}</slot>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="mc-drawer-close"
|
||||
:title="closeLabel"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mc-drawer-body" :class="bodyClass">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" class="mc-drawer-footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
// Frosted-glass right-side drawer with a spring slide, circular close,
|
||||
// 40px icon hero, and a mobile bottom-sheet fallback. The shared visual
|
||||
// language for every detail/edit/diagnostic surface in the app.
|
||||
//
|
||||
// Three sizes — sm (480) for compact status drawers, md (640) for
|
||||
// detail/edit drawers, lg (880) for full takeover editors. The
|
||||
// bodyClass escape hatch lets callers swap padding/overflow rules
|
||||
// without bloating this component's API.
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
title: string
|
||||
subtitle?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
bodyClass?: string
|
||||
closeLabel?: string
|
||||
}>(),
|
||||
{
|
||||
subtitle: '',
|
||||
size: 'sm',
|
||||
bodyClass: '',
|
||||
closeLabel: 'Close',
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
const overlayClass = computed(() => ({
|
||||
'mc-drawer-overlay--md': props.size === 'md',
|
||||
'mc-drawer-overlay--lg': props.size === 'lg',
|
||||
}))
|
||||
const panelClass = computed(() => ({
|
||||
'mc-drawer-panel--md': props.size === 'md',
|
||||
'mc-drawer-panel--lg': props.size === 'lg',
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mc-drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(20, 14, 10, 0.32);
|
||||
backdrop-filter: blur(8px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(140%);
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
:global(html.dark .mc-drawer-overlay) {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.mc-drawer-panel {
|
||||
width: 480px;
|
||||
max-width: 92vw;
|
||||
height: 100%;
|
||||
background: rgba(255, 250, 245, 0.78);
|
||||
backdrop-filter: blur(48px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(48px) saturate(180%);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: -24px 0 60px rgba(25, 14, 8, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: mc-drawer-slide 0.36s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
transition: width 0.32s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
.mc-drawer-panel--md {
|
||||
width: 640px;
|
||||
}
|
||||
.mc-drawer-panel--lg {
|
||||
width: 880px;
|
||||
}
|
||||
:global(html.dark .mc-drawer-panel) {
|
||||
background: rgba(32, 26, 22, 0.82);
|
||||
border-left-color: rgba(255, 255, 255, 0.10);
|
||||
box-shadow: -24px 0 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@keyframes mc-drawer-slide {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.mc-drawer-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 22px 26px 18px;
|
||||
border-bottom: 1px solid rgba(123, 88, 67, 0.10);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
:global(html.dark .mc-drawer-header) {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.mc-drawer-header__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mc-drawer-icon-shell {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
box-shadow: inset 0 0 0 1px rgba(123, 88, 67, 0.10);
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
:global(html.dark .mc-drawer-icon-shell) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.mc-drawer-titles {
|
||||
min-width: 0;
|
||||
}
|
||||
.mc-drawer-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mc-text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mc-drawer-subtitle {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--mc-text-tertiary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mc-drawer-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
color: var(--mc-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.mc-drawer-close:hover {
|
||||
background: rgba(123, 88, 67, 0.08);
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
:global(html.dark .mc-drawer-close:hover) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
|
||||
/* Body fills the panel and provides a flex-column shell so consumers
|
||||
* can use `flex: 1` to stretch content (e.g. takeover editors).
|
||||
* Padding, gap, and inner layout are intentionally the consumer's
|
||||
* call — keeps the override surface zero. */
|
||||
.mc-drawer-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mc-drawer-footer {
|
||||
padding: 14px 22px 18px;
|
||||
border-top: 1px solid rgba(123, 88, 67, 0.10);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
:global(html.dark .mc-drawer-footer) {
|
||||
border-top-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.mc-drawer-fade-enter-active,
|
||||
.mc-drawer-fade-leave-active {
|
||||
transition: opacity 0.22s ease;
|
||||
}
|
||||
.mc-drawer-fade-enter-from,
|
||||
.mc-drawer-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mc-drawer-overlay {
|
||||
justify-content: stretch;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.mc-drawer-panel,
|
||||
.mc-drawer-panel--lg {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 92vh;
|
||||
border-left: 0;
|
||||
border-top-left-radius: 18px;
|
||||
border-top-right-radius: 18px;
|
||||
animation: mc-drawer-sheet-up 0.36s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
@keyframes mc-drawer-sheet-up {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -510,6 +510,7 @@ export default {
|
||||
},
|
||||
doctor: {
|
||||
title: 'System Diagnostics',
|
||||
subtitle: 'Local instance health snapshot',
|
||||
checking: 'Checking...',
|
||||
allGood: 'All systems healthy',
|
||||
hasWarnings: '{count} warning(s)',
|
||||
@ -517,6 +518,8 @@ export default {
|
||||
refresh: 'Re-check',
|
||||
lastChecked: 'Checked {time} ago',
|
||||
diagnose: 'Diagnose',
|
||||
checksPassed: '{healthy}/{total} checks passed',
|
||||
checksLoading: 'Sampling system signals',
|
||||
},
|
||||
// Issue #81: provider-level hint / status text shared across chat popup and ModelSelector.
|
||||
provider: {
|
||||
|
||||
@ -1823,6 +1823,7 @@ export default {
|
||||
},
|
||||
doctor: {
|
||||
title: '系统诊断',
|
||||
subtitle: '本地实例健康概览',
|
||||
checking: '检查中...',
|
||||
allGood: '所有系统正常',
|
||||
hasWarnings: '{count} 个警告',
|
||||
@ -1830,6 +1831,8 @@ export default {
|
||||
refresh: '重新检查',
|
||||
lastChecked: '{time} 前检查',
|
||||
diagnose: '诊断问题',
|
||||
checksPassed: '{healthy}/{total} 项检查通过',
|
||||
checksLoading: '正在采样系统指标',
|
||||
},
|
||||
wiki: {
|
||||
kicker: '知识引擎',
|
||||
|
||||
@ -1,60 +1,95 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="drawer-overlay">
|
||||
<div class="drawer-panel">
|
||||
<div class="drawer-header">
|
||||
<h2 class="drawer-title">{{ t('doctor.title') }}</h2>
|
||||
<button class="drawer-close" @click="emit('close')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<MateDrawer
|
||||
:visible="visible"
|
||||
:title="t('doctor.title')"
|
||||
:subtitle="t('doctor.subtitle')"
|
||||
:close-label="t('common.close')"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<template #icon>
|
||||
<!-- Heartbeat glyph — Apple Health metaphor: this drawer reports
|
||||
vital signs of the local instance, not just status flags. -->
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<!-- Overall status banner -->
|
||||
<div class="status-banner" :class="health?.overall || 'loading'">
|
||||
<span class="status-dot"></span>
|
||||
<span v-if="loading">{{ t('doctor.checking') }}</span>
|
||||
<span v-else-if="health?.overall === 'healthy'">{{ t('doctor.allGood') }}</span>
|
||||
<span v-else-if="health?.overall === 'warning'">{{ t('doctor.hasWarnings', { count: warningCount }) }}</span>
|
||||
<span v-else-if="health?.overall === 'error'">{{ t('doctor.hasErrors', { count: errorCount }) }}</span>
|
||||
<div class="doctor-stack">
|
||||
<!-- Status hero: the first fact the user reads. A frosted card
|
||||
carries the overall verdict; the colored dot does the
|
||||
semantic work, the background stays calm. -->
|
||||
<div class="status-hero" :class="`status-hero--${heroState}`">
|
||||
<span class="status-hero__dot"></span>
|
||||
<div class="status-hero__text">
|
||||
<h4 class="status-hero__headline">{{ heroHeadline }}</h4>
|
||||
<p class="status-hero__sub">{{ heroSub }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check list -->
|
||||
<div class="check-list">
|
||||
<div v-for="check in health?.checks" :key="check.name" class="check-item">
|
||||
<span class="check-dot" :class="check.status"></span>
|
||||
<div class="check-info">
|
||||
<div class="check-name">{{ check.name }}</div>
|
||||
<div class="check-message">{{ check.message }}</div>
|
||||
</div>
|
||||
<router-link
|
||||
v-if="check.action && check.status !== 'healthy'"
|
||||
:to="check.action.route"
|
||||
class="check-action"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ check.action.label }}
|
||||
</router-link>
|
||||
<!-- Check list — each row is a frosted card, not a bordered row. -->
|
||||
<div class="check-list">
|
||||
<div
|
||||
v-for="check in health?.checks"
|
||||
:key="check.name"
|
||||
class="check-card"
|
||||
>
|
||||
<span class="check-card__dot" :class="`check-card__dot--${check.status}`"></span>
|
||||
<div class="check-card__info">
|
||||
<div class="check-card__name">{{ check.name }}</div>
|
||||
<div class="check-card__message">{{ check.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Refresh -->
|
||||
<div class="drawer-footer">
|
||||
<button class="btn-secondary" @click="fetchHealth" :disabled="loading">
|
||||
{{ loading ? t('doctor.checking') : t('doctor.refresh') }}
|
||||
</button>
|
||||
<span v-if="lastChecked" class="last-checked">{{ t('doctor.lastChecked', { time: lastCheckedText }) }}</span>
|
||||
<router-link
|
||||
v-if="check.action && check.status !== 'healthy'"
|
||||
:to="check.action.route"
|
||||
class="check-card__action"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ check.action.label }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<template #footer>
|
||||
<button class="footer-refresh" :disabled="loading" @click="fetchHealth">
|
||||
<svg
|
||||
v-if="!loading"
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="23 4 23 10 17 10" />
|
||||
<polyline points="1 20 1 14 7 14" />
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</svg>
|
||||
{{ loading ? t('doctor.checking') : t('doctor.refresh') }}
|
||||
</button>
|
||||
<span v-if="lastChecked" class="footer-stamp">
|
||||
{{ t('doctor.lastChecked', { time: lastCheckedText }) }}
|
||||
</span>
|
||||
</template>
|
||||
</MateDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { http } from '@/api/index'
|
||||
import MateDrawer from '@/components/common/MateDrawer.vue'
|
||||
|
||||
interface HealthAction { label: string; route: string }
|
||||
interface HealthCheck { name: string; status: string; message: string; action?: HealthAction }
|
||||
@ -68,8 +103,35 @@ const health = ref<HealthResponse | null>(null)
|
||||
const loading = ref(false)
|
||||
const lastChecked = ref<Date | null>(null)
|
||||
|
||||
const warningCount = computed(() => health.value?.checks.filter(c => c.status === 'warning').length || 0)
|
||||
const errorCount = computed(() => health.value?.checks.filter(c => c.status === 'error').length || 0)
|
||||
const totalChecks = computed(() => health.value?.checks.length || 0)
|
||||
const healthyChecks = computed(
|
||||
() => health.value?.checks.filter(c => c.status === 'healthy').length || 0,
|
||||
)
|
||||
const warningCount = computed(
|
||||
() => health.value?.checks.filter(c => c.status === 'warning').length || 0,
|
||||
)
|
||||
const errorCount = computed(
|
||||
() => health.value?.checks.filter(c => c.status === 'error').length || 0,
|
||||
)
|
||||
|
||||
const heroState = computed(() => {
|
||||
if (loading.value && !health.value) return 'loading'
|
||||
return health.value?.overall || 'loading'
|
||||
})
|
||||
|
||||
const heroHeadline = computed(() => {
|
||||
if (heroState.value === 'loading') return t('doctor.checking')
|
||||
if (heroState.value === 'healthy') return t('doctor.allGood')
|
||||
if (heroState.value === 'warning') return t('doctor.hasWarnings', { count: warningCount.value })
|
||||
if (heroState.value === 'error') return t('doctor.hasErrors', { count: errorCount.value })
|
||||
return ''
|
||||
})
|
||||
|
||||
const heroSub = computed(() => {
|
||||
if (heroState.value === 'loading') return t('doctor.checksLoading')
|
||||
return t('doctor.checksPassed', { healthy: healthyChecks.value, total: totalChecks.value })
|
||||
})
|
||||
|
||||
const lastCheckedText = computed(() => {
|
||||
if (!lastChecked.value) return ''
|
||||
const secs = Math.floor((Date.now() - lastChecked.value.getTime()) / 1000)
|
||||
@ -97,37 +159,177 @@ watch(() => props.visible, (v) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 1500; display: flex; justify-content: flex-end; }
|
||||
.drawer-panel { width: 400px; max-width: 90vw; height: 100%; background: var(--mc-bg-elevated); border-left: 1px solid var(--mc-border); display: flex; flex-direction: column; animation: slide-in 0.2s ease; }
|
||||
@keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
|
||||
/* Body layout — MateDrawer's body is minimal; we own padding + gap. */
|
||||
.doctor-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 18px 22px 22px;
|
||||
}
|
||||
|
||||
.drawer-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid var(--mc-border-light); }
|
||||
.drawer-title { font-size: 16px; font-weight: 600; color: var(--mc-text-primary); margin: 0; }
|
||||
.drawer-close { width: 32px; height: 32px; border: none; background: none; cursor: pointer; color: var(--mc-text-tertiary); display: flex; align-items: center; justify-content: center; border-radius: 6px; }
|
||||
.drawer-close:hover { background: var(--mc-bg-sunken); }
|
||||
/* Status hero — frosted card with a colored dot doing the semantic work. */
|
||||
.status-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 1px 3px rgba(25, 14, 8, 0.04);
|
||||
}
|
||||
:global(html.dark .status-hero) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.status-hero__dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
background: var(--mc-text-tertiary);
|
||||
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.status-hero--loading .status-hero__dot {
|
||||
background: var(--mc-text-tertiary);
|
||||
animation: status-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
.status-hero--healthy .status-hero__dot {
|
||||
background: var(--mc-success);
|
||||
box-shadow: 0 0 0 4px rgba(90, 138, 90, 0.12);
|
||||
}
|
||||
.status-hero--warning .status-hero__dot {
|
||||
background: var(--mc-primary);
|
||||
box-shadow: 0 0 0 4px rgba(217, 119, 87, 0.14);
|
||||
}
|
||||
.status-hero--error .status-hero__dot {
|
||||
background: var(--mc-danger);
|
||||
box-shadow: 0 0 0 4px rgba(200, 60, 60, 0.14);
|
||||
}
|
||||
@keyframes status-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
.status-hero__text {
|
||||
min-width: 0;
|
||||
}
|
||||
.status-hero__headline {
|
||||
margin: 0 0 2px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.005em;
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
.status-hero__sub {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
|
||||
.status-banner { display: flex; align-items: center; gap: 10px; padding: 12px 24px; font-size: 14px; font-weight: 500; }
|
||||
.status-banner.healthy { color: var(--mc-success); background: rgba(90,138,90,0.08); }
|
||||
.status-banner.warning { color: var(--mc-primary); background: var(--mc-primary-bg); }
|
||||
.status-banner.error { color: var(--mc-danger); background: var(--mc-danger-bg); }
|
||||
.status-banner.loading { color: var(--mc-text-secondary); background: var(--mc-bg-sunken); }
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
|
||||
/* Check list — frosted cards, no visible borders. The dot carries
|
||||
the status, the action sits as a pill on the right. */
|
||||
.check-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.check-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
box-shadow: 0 1px 2px rgba(25, 14, 8, 0.03);
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.check-card:hover {
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
:global(html.dark .check-card) {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
:global(html.dark .check-card:hover) {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
.check-card__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.check-card__dot--healthy { background: var(--mc-success); }
|
||||
.check-card__dot--warning { background: var(--mc-primary); }
|
||||
.check-card__dot--error { background: var(--mc-danger); }
|
||||
.check-card__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.check-card__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.005em;
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
.check-card__message {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--mc-text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.check-card__action {
|
||||
flex-shrink: 0;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: var(--mc-primary);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.check-card__action:hover {
|
||||
background: var(--mc-primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.check-list { flex: 1; overflow-y: auto; padding: 16px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.check-item { display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: var(--mc-bg); border-radius: 8px; border: 1px solid var(--mc-border-light); }
|
||||
.check-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0; }
|
||||
.check-dot.healthy { background: var(--mc-success); }
|
||||
.check-dot.warning { background: var(--mc-primary); }
|
||||
.check-dot.error { background: var(--mc-danger); }
|
||||
.check-info { flex: 1; min-width: 0; }
|
||||
.check-name { font-size: 13px; font-weight: 500; color: var(--mc-text-primary); }
|
||||
.check-message { font-size: 12px; color: var(--mc-text-secondary); margin-top: 2px; }
|
||||
.check-action { font-size: 12px; color: var(--mc-primary); text-decoration: none; white-space: nowrap; padding: 4px 10px; border: 1px solid var(--mc-primary); border-radius: 6px; flex-shrink: 0; }
|
||||
.check-action:hover { background: var(--mc-primary-bg); }
|
||||
|
||||
.drawer-footer { padding: 16px 24px; border-top: 1px solid var(--mc-border-light); display: flex; align-items: center; gap: 12px; }
|
||||
.btn-secondary { padding: 6px 14px; background: var(--mc-bg-elevated); color: var(--mc-text-primary); border: 1px solid var(--mc-border); border-radius: 6px; font-size: 13px; cursor: pointer; }
|
||||
.btn-secondary:hover { background: var(--mc-bg-sunken); }
|
||||
.btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.last-checked { font-size: 11px; color: var(--mc-text-tertiary); }
|
||||
/* Footer refresh — pill secondary button to match the rest of the app. */
|
||||
.footer-refresh {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
box-shadow: inset 0 0 0 1px rgba(123, 88, 67, 0.12);
|
||||
color: var(--mc-text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.footer-refresh:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
box-shadow: inset 0 0 0 1px rgba(123, 88, 67, 0.20);
|
||||
}
|
||||
.footer-refresh:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:global(html.dark .footer-refresh) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
:global(html.dark .footer-refresh:hover:not(:disabled)) {
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
.footer-stamp {
|
||||
font-size: 11px;
|
||||
color: var(--mc-text-tertiary);
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -199,41 +199,21 @@
|
||||
:skill-name="preflightSkillName"
|
||||
/>
|
||||
|
||||
<!-- Skill detail drawer.
|
||||
Mirrors Settings/Models/AddProviderDrawer.vue — Teleport + frosted
|
||||
glass, iOS-spring slide, mobile bottom-sheet, dark variants. The
|
||||
old el-drawer was off-brand and didn't match the rest of the app. -->
|
||||
<Teleport to="body">
|
||||
<Transition name="mc-drawer-fade">
|
||||
<div
|
||||
v-if="detailDrawerVisible && detailSkill"
|
||||
class="mc-drawer-overlay"
|
||||
:class="{ 'mc-drawer-overlay--wide': editingBody }"
|
||||
@click.self="closeDetailDrawer"
|
||||
>
|
||||
<div class="mc-drawer-panel" :class="{ 'mc-drawer-panel--wide': editingBody }">
|
||||
<div class="mc-drawer-header">
|
||||
<div class="mc-drawer-header__meta">
|
||||
<span class="mc-drawer-icon-shell">
|
||||
<SkillIcon :value="detailSkill.icon" :size="24" />
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="mc-drawer-title">{{ resolveSkillName(detailSkill) }}</h3>
|
||||
<p class="mc-drawer-subtitle">
|
||||
{{ editingBody ? t('skills.detail.editingSource') : (detailSkill.description || detailSkill.name) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="mc-drawer-close"
|
||||
:title="t('common.cancel')"
|
||||
@click="closeDetailDrawer"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Skill detail/edit drawer — frosted shell comes from MateDrawer;
|
||||
the existing .mc-drawer-content wrapper below stays so the
|
||||
tab/block/takeover styles keep working without renaming. -->
|
||||
<MateDrawer
|
||||
:visible="detailDrawerVisible && !!detailSkill"
|
||||
:title="detailSkill ? resolveSkillName(detailSkill) : ''"
|
||||
:subtitle="detailSkill ? (editingBody ? t('skills.detail.editingSource') : (detailSkill.description || detailSkill.name)) : ''"
|
||||
:size="editingBody ? 'lg' : 'md'"
|
||||
:close-label="t('common.cancel')"
|
||||
@close="closeDetailDrawer"
|
||||
>
|
||||
<template v-if="detailSkill" #icon>
|
||||
<SkillIcon :value="detailSkill.icon" :size="24" />
|
||||
</template>
|
||||
<template v-if="detailSkill">
|
||||
|
||||
<!-- Body-edit takeover: the entire drawer becomes one editor.
|
||||
Tabs and other blocks are hidden so the user can think in
|
||||
@ -595,10 +575,8 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
</MateDrawer>
|
||||
|
||||
<!-- New-skill modal (Layer-1 redesign).
|
||||
Reduced to 2 fields. The user's job here is "name it and confirm
|
||||
@ -683,6 +661,7 @@ import ImportHubDialog from '@/components/skill/ImportHubDialog.vue'
|
||||
import PreflightInstallDialog from '@/components/skill/PreflightInstallDialog.vue'
|
||||
import SkillSecretsPanel from '@/components/skill/SkillSecretsPanel.vue'
|
||||
import McPagination from '@/components/common/McPagination.vue'
|
||||
import MateDrawer from '@/components/common/MateDrawer.vue'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import SkillIconPicker from '@/components/common/SkillIconPicker.vue'
|
||||
import { mcConfirm } from '@/components/common/useConfirm'
|
||||
@ -2169,128 +2148,10 @@ html.dark .scan-finding-item { background: rgba(255, 255, 255, 0.05); }
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* MateClaw frosted-glass drawer
|
||||
* Mirrors Settings/Models/AddProviderDrawer.vue so the skill
|
||||
* detail surface lives in the same visual language as the rest
|
||||
* of the app — depth via translucency, not borders.
|
||||
* ============================================================ */
|
||||
.mc-drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(20, 14, 10, 0.32);
|
||||
backdrop-filter: blur(8px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(140%);
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
:global(html.dark .mc-drawer-overlay) {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.mc-drawer-panel {
|
||||
width: 640px;
|
||||
max-width: 92vw;
|
||||
height: 100%;
|
||||
background: rgba(255, 250, 245, 0.78);
|
||||
backdrop-filter: blur(48px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(48px) saturate(180%);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: -24px 0 60px rgba(25, 14, 8, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: mc-drawer-slide 0.36s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
transition: width 0.32s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
.mc-drawer-panel--wide {
|
||||
/* Editing SKILL.md needs room — 640 was cramped for code authoring. */
|
||||
width: 880px;
|
||||
}
|
||||
:global(html.dark .mc-drawer-panel) {
|
||||
background: rgba(32, 26, 22, 0.82);
|
||||
border-left-color: rgba(255, 255, 255, 0.10);
|
||||
box-shadow: -24px 0 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@keyframes mc-drawer-slide {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.mc-drawer-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 22px 26px 18px;
|
||||
border-bottom: 1px solid rgba(123, 88, 67, 0.10);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
:global(html.dark .mc-drawer-header) {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.mc-drawer-header__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mc-drawer-icon-shell {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
box-shadow: inset 0 0 0 1px rgba(123, 88, 67, 0.10);
|
||||
}
|
||||
:global(html.dark .mc-drawer-icon-shell) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.mc-drawer-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mc-text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mc-drawer-subtitle {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--mc-text-tertiary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mc-drawer-close {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
color: var(--mc-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.mc-drawer-close:hover {
|
||||
background: rgba(123, 88, 67, 0.08);
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
:global(html.dark .mc-drawer-close:hover) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
|
||||
/* Drawer shell (overlay, panel, header, close, mobile sheet) comes
|
||||
* from MateDrawer; its body is a bare flex-column shell. This
|
||||
* .mc-drawer-content wrapper is what hosts either tabbed sections
|
||||
* (default) or a full-bleed SKILL.md editor (takeover). */
|
||||
.mc-drawer-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@ -2487,38 +2348,4 @@ html.dark .scan-finding-item { background: rgba(255, 255, 255, 0.05); }
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mc-drawer-fade-enter-active,
|
||||
.mc-drawer-fade-leave-active {
|
||||
transition: opacity 0.22s ease;
|
||||
}
|
||||
.mc-drawer-fade-enter-from,
|
||||
.mc-drawer-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Mobile: bottom sheet so the drawer doesn't crush 92vw. */
|
||||
@media (max-width: 768px) {
|
||||
.mc-drawer-overlay {
|
||||
justify-content: stretch;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.mc-drawer-panel,
|
||||
.mc-drawer-panel--wide {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 92vh;
|
||||
border-left: 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
animation: mc-drawer-slide-up 0.32s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
:global(html.dark .mc-drawer-panel) {
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
@keyframes mc-drawer-slide-up {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user