mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
refactor(approval-grants-ui): strip Element Plus, reuse project list+pagination
This commit is contained in:
parent
6a754f5bfa
commit
b34de4e078
@ -3732,6 +3732,7 @@ export default {
|
||||
manage: 'Manage auto-approve rules...',
|
||||
},
|
||||
chipLabel: 'Auto-approve active ({count})',
|
||||
chipShort: 'Auto-approve {count}',
|
||||
menu: 'Auto-approve',
|
||||
title: 'Auto-approve rules',
|
||||
desc: 'Rules let specific tool calls skip manual approval. Safety-floor patterns (rm -rf /, pipe-to-shell, etc.) always apply, and CRITICAL severity always falls back to human approval.',
|
||||
|
||||
@ -3824,6 +3824,7 @@ export default {
|
||||
manage: '管理自动批准策略...',
|
||||
},
|
||||
chipLabel: '自动批准已启用 ({count})',
|
||||
chipShort: '自动批准 {count}',
|
||||
menu: '自动批准',
|
||||
title: '自动批准策略',
|
||||
desc: '策略让特定工具调用跳过人审。地板规则(如 rm -rf /、pipe-to-shell)始终生效,CRITICAL 严重度永远人审。',
|
||||
|
||||
@ -16,17 +16,12 @@
|
||||
<div class="section-header">
|
||||
<div class="section-header__title">
|
||||
<h2 class="section-title">{{ t('approval.grant.title') }}</h2>
|
||||
<el-tag
|
||||
v-if="total > 0"
|
||||
type="warning"
|
||||
size="small"
|
||||
effect="light"
|
||||
round
|
||||
disable-transitions
|
||||
class="active-summary"
|
||||
>
|
||||
<!-- Inline summary pill — same "danger-tinted info" tokens as the
|
||||
sidebar chip so the two surfaces feel like one design language.
|
||||
No el-tag here: keeps the page entirely native + token-driven. -->
|
||||
<span v-if="total > 0" class="summary-pill">
|
||||
{{ t('approval.grant.chipLabel', { count: total }) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<p class="section-desc">{{ t('approval.grant.desc') }}</p>
|
||||
</div>
|
||||
<div class="section-header__actions">
|
||||
@ -54,113 +49,93 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:empty-text="t('approval.grant.empty')"
|
||||
size="small"
|
||||
stripe
|
||||
>
|
||||
<el-table-column :label="t('approval.grant.columns.scope')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="scopeTagType(row.scopeType)"
|
||||
size="small"
|
||||
effect="light"
|
||||
disable-transitions
|
||||
>
|
||||
{{ t(`approval.grant.scope.${scopeI18nKey(row.scopeType)}`) }}
|
||||
</el-tag>
|
||||
<span class="scope-id">{{ row.scopeId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!--
|
||||
Table + pagination — native HTML, no Element Plus components. Mirrors
|
||||
the ToolGuard / AuditLogs pattern (rules-table-wrapper + .rules-table
|
||||
+ .severity-badge / .action-btn from shared.css) so this page reads
|
||||
like any other Security sub-view. Pagination uses the shared
|
||||
McPagination component, which is also fully native (no el-pagination).
|
||||
-->
|
||||
<div class="rules-table-wrapper">
|
||||
<table class="rules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('approval.grant.columns.scope') }}</th>
|
||||
<th>{{ t('approval.grant.columns.tool') }}</th>
|
||||
<th>{{ t('approval.grant.columns.rule') }}</th>
|
||||
<th>{{ t('approval.grant.columns.severity') }}</th>
|
||||
<th>{{ t('approval.grant.columns.kind') }}</th>
|
||||
<th>{{ t('approval.grant.columns.expire') }}</th>
|
||||
<th>{{ t('approval.grant.columns.grantedBy') }}</th>
|
||||
<th>{{ t('approval.grant.columns.note') }}</th>
|
||||
<th class="col-actions">{{ t('approval.grant.columns.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in rows"
|
||||
:key="row.id"
|
||||
:class="{ 'row-revoked': row.revoked === 1 }"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
class="scope-badge"
|
||||
:class="`scope-${scopeI18nKey(row.scopeType)}`"
|
||||
>
|
||||
{{ t(`approval.grant.scope.${scopeI18nKey(row.scopeType)}`) }}
|
||||
</span>
|
||||
<div class="scope-id" :title="row.scopeId">{{ row.scopeId }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<code v-if="row.toolName" class="mono">{{ row.toolName }}</code>
|
||||
<span v-else class="severity-badge severity-high">∗ any</span>
|
||||
</td>
|
||||
<td>
|
||||
<code v-if="row.ruleId" class="mono">{{ row.ruleId }}</code>
|
||||
<span v-else class="muted">∗</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="severity-badge"
|
||||
:class="`severity-${row.maxSeverity?.toLowerCase()}`"
|
||||
>
|
||||
{{ row.maxSeverity }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ t(`approval.grant.kind.${kindI18nKey(row.grantKind)}`) }}</td>
|
||||
<td class="muted">{{ formatDate(row.expireAt) }}</td>
|
||||
<td>{{ row.grantedBy }}</td>
|
||||
<td class="note-cell" :title="row.note || ''">{{ row.note }}</td>
|
||||
<td>
|
||||
<button
|
||||
v-if="row.revoked === 0"
|
||||
class="action-btn danger"
|
||||
:title="t('approval.grant.revokeBtn')"
|
||||
@click="confirmRevoke(row)"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span v-else class="muted">{{ t('common.revoked') }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="loading" class="empty-state">{{ t('common.loading') }}</div>
|
||||
<div v-else-if="!rows.length" class="empty-state">{{ t('approval.grant.empty') }}</div>
|
||||
</div>
|
||||
|
||||
<el-table-column
|
||||
:label="t('approval.grant.columns.tool')"
|
||||
prop="toolName"
|
||||
min-width="140"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<code v-if="row.toolName" class="mono">{{ row.toolName }}</code>
|
||||
<el-tag v-else type="danger" size="small" effect="dark">∗ any</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('approval.grant.columns.rule')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<code v-if="row.ruleId" class="mono">{{ row.ruleId }}</code>
|
||||
<span v-else class="muted">∗</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
:label="t('approval.grant.columns.severity')"
|
||||
prop="maxSeverity"
|
||||
width="110"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="severityTagType(row.maxSeverity)" size="small" disable-transitions>
|
||||
{{ row.maxSeverity }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('approval.grant.columns.kind')" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ t(`approval.grant.kind.${kindI18nKey(row.grantKind)}`) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('approval.grant.columns.expire')" width="160">
|
||||
<template #default="{ row }">
|
||||
<span class="muted">{{ formatDate(row.expireAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
:label="t('approval.grant.columns.grantedBy')"
|
||||
prop="grantedBy"
|
||||
width="120"
|
||||
/>
|
||||
|
||||
<el-table-column :label="t('approval.grant.columns.note')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span :title="row.note || ''" class="note-cell">{{ row.note }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('approval.grant.columns.actions')" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.revoked === 0"
|
||||
:icon="Delete"
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
@click="confirmRevoke(row)"
|
||||
>
|
||||
{{ t('approval.grant.revokeBtn') }}
|
||||
</el-button>
|
||||
<el-tag v-else type="info" size="small" effect="plain" disable-transitions>
|
||||
{{ t('common.revoked') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
class="grants-pagination"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
<div v-if="total > 0" class="grants-pagination-row">
|
||||
<McPagination
|
||||
:page="currentPage"
|
||||
:size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
small
|
||||
@size-change="loadGrants"
|
||||
@current-change="loadGrants"
|
||||
:sizes="[10, 20, 50, 100]"
|
||||
@update:page="onPageChange"
|
||||
@update:size="onSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -312,6 +287,7 @@ import {
|
||||
WarningFilled,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { approvalApi } from '@/api'
|
||||
import McPagination from '@/components/common/McPagination.vue'
|
||||
import type {
|
||||
ApprovalGrant,
|
||||
CreateGrantPayload,
|
||||
@ -365,6 +341,16 @@ const requiresPassword = computed(() => {
|
||||
return noTool && (form.scopeType === 'WORKSPACE' || form.scopeType === 'AGENT')
|
||||
})
|
||||
|
||||
function onPageChange(p: number) {
|
||||
currentPage.value = p
|
||||
loadGrants()
|
||||
}
|
||||
function onSizeChange(s: number) {
|
||||
pageSize.value = s
|
||||
currentPage.value = 1
|
||||
loadGrants()
|
||||
}
|
||||
|
||||
async function loadGrants() {
|
||||
loading.value = true
|
||||
try {
|
||||
@ -471,23 +457,6 @@ function scopeI18nKey(scope: GrantScope): string {
|
||||
}
|
||||
}
|
||||
|
||||
function scopeTagType(scope: GrantScope): 'primary' | 'success' | 'warning' | 'danger' | 'info' {
|
||||
switch (scope) {
|
||||
case 'CONVERSATION': return 'primary'
|
||||
case 'AGENT': return 'warning'
|
||||
case 'USER': return 'success'
|
||||
case 'WORKSPACE': return 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
function severityTagType(sev: GrantSeverity): 'success' | 'warning' | 'danger' {
|
||||
switch (sev) {
|
||||
case 'LOW': return 'success'
|
||||
case 'MEDIUM': return 'warning'
|
||||
case 'HIGH': return 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
function kindI18nKey(kind: GrantKind): string {
|
||||
switch (kind) {
|
||||
case 'ALWAYS': return 'always'
|
||||
@ -523,8 +492,19 @@ onMounted(loadGrants)
|
||||
flex-basis: 100%;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
.active-summary {
|
||||
/* Summary pill — reuses the same danger-tinted tokens as the sidebar chip
|
||||
so the two surfaces feel like one design language; no el-tag here. */
|
||||
.summary-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--mc-danger-bg, rgba(192, 57, 43, 0.12));
|
||||
color: var(--mc-danger, #C0392B);
|
||||
border: 1px solid var(--mc-danger-border, rgba(192, 57, 43, 0.4));
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Header actions: matches McpServers / ToolGuard layout (.section-header__actions
|
||||
@ -567,17 +547,36 @@ onMounted(loadGrants)
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Scope badge — same family as .severity-badge in shared.css, 4 token-driven
|
||||
color variants for CONVERSATION / AGENT / USER / WORKSPACE. */
|
||||
.scope-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.scope-conversation { background: rgba(59, 130, 246, 0.12); color: #3b82f6; }
|
||||
.scope-agent { background: rgba(245, 158, 11, 0.12); color: #f59e0b; }
|
||||
.scope-user { background: rgba(16, 185, 129, 0.12); color: #10b981; }
|
||||
.scope-workspace { background: var(--mc-danger-bg, rgba(192, 57, 43, 0.12));
|
||||
color: var(--mc-danger, #C0392B); }
|
||||
.scope-id {
|
||||
margin-left: 8px;
|
||||
margin-top: 2px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--mc-text-tertiary, #94a3b8);
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
background: var(--mc-surface-tertiary, #f1f5f9);
|
||||
background: var(--mc-surface-tertiary, var(--mc-bg-muted, rgba(0, 0, 0, 0.04)));
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@ -596,8 +595,29 @@ onMounted(loadGrants)
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.grants-pagination {
|
||||
margin-top: 16px;
|
||||
/* Revoked rows: dim out so live rows stay visually dominant. */
|
||||
.row-revoked { opacity: 0.55; }
|
||||
.row-revoked .scope-badge { filter: grayscale(0.5); }
|
||||
|
||||
/* Actions column header sits right-aligned, matches ToolGuard. */
|
||||
.col-actions { text-align: center; width: 80px; }
|
||||
|
||||
/* This page has more columns than ToolGuard / AuditLogs, so the wrapper needs
|
||||
to allow horizontal scroll on narrow viewports — shared.css uses
|
||||
overflow:hidden which would chop the rightmost columns off. Scoped override
|
||||
only affects this view; the shared style stays untouched. */
|
||||
.rules-table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.rules-table {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
/* Pagination row — right-aligned beneath the table. McPagination provides
|
||||
its own pill background, so just lay it out. */
|
||||
.grants-pagination-row {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
@ -80,11 +80,34 @@
|
||||
<!-- 底部 -->
|
||||
<div class="sidebar-footer">
|
||||
<template v-if="!sidebarCollapsed || isMobile">
|
||||
<!-- Doctor 健康指示器 -->
|
||||
<button class="health-indicator" :class="healthStatus" @click="showDoctor = true" :title="t('doctor.title')">
|
||||
<span class="health-dot"></span>
|
||||
<span class="health-label">{{ t('doctor.title') }}</span>
|
||||
</button>
|
||||
<!--
|
||||
Status row: two side-by-side status cards. Doctor health on the left
|
||||
stays always visible; the auto-approve chip on the right only renders
|
||||
when at least one grant is active. When the chip is hidden, flex
|
||||
naturally lets the doctor card expand to full width again — so the
|
||||
row never wastes vertical space the way a stacked banner did.
|
||||
-->
|
||||
<div class="footer-status-row">
|
||||
<button
|
||||
class="health-indicator"
|
||||
:class="[healthStatus, { 'is-half': autoApproveSummary && autoApproveSummary.count > 0 }]"
|
||||
@click="showDoctor = true"
|
||||
:title="t('doctor.title')"
|
||||
>
|
||||
<span class="health-dot"></span>
|
||||
<span class="health-label">{{ t('doctor.title') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="autoApproveSummary && autoApproveSummary.count > 0"
|
||||
class="auto-approve-chip"
|
||||
@click="goAutoApproveSettings"
|
||||
:title="t('approval.grant.title')"
|
||||
>
|
||||
<span class="auto-approve-chip__dot"></span>
|
||||
<el-icon :size="13"><Unlock /></el-icon>
|
||||
<span class="auto-approve-chip__label">{{ t('approval.grant.chipShort', { count: autoApproveSummary.count }) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-utility-card">
|
||||
<div class="compact-utility-row">
|
||||
@ -133,18 +156,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Auto-approve chip — red on purpose: security-reducing setting needs
|
||||
persistent visibility, not a friendly green badge. -->
|
||||
<button
|
||||
v-if="autoApproveSummary && autoApproveSummary.count > 0"
|
||||
class="auto-approve-chip"
|
||||
@click="goAutoApproveSettings"
|
||||
:title="t('approval.grant.title')"
|
||||
>
|
||||
<el-icon :size="14"><Unlock /></el-icon>
|
||||
<span>{{ t('approval.grant.chipLabel', { count: autoApproveSummary.count }) }}</span>
|
||||
</button>
|
||||
|
||||
<div class="shortcuts-hint" :title="shortcutsHintText">
|
||||
<kbd>Ctrl+K</kbd>
|
||||
<span>{{ t('nav.shortcutAgents') }}</span>
|
||||
@ -805,25 +816,93 @@ watch(() => workspaceStore.currentWorkspaceId, () => {
|
||||
backdrop-filter: blur(14px);
|
||||
position: relative;
|
||||
}
|
||||
.health-indicator { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border: 1px solid var(--mc-border-light); background: var(--mc-bg-muted); border-radius: 12px; cursor: pointer; color: var(--mc-text-secondary); font-size: 12px; margin-bottom: 8px; }
|
||||
/* Status row: doctor health + (optional) auto-approve chip side by side, so
|
||||
the footer never gives up a whole banner-row for a single state badge. When
|
||||
the chip is hidden, .health-indicator naturally expands back to full width. */
|
||||
.footer-status-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.health-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--mc-border-light);
|
||||
background: var(--mc-bg-muted);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--mc-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.health-indicator:hover { background: var(--mc-bg-sunken); }
|
||||
.health-indicator .health-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* When the chip is showing, the health card yields half of the row so the
|
||||
two states stay visually balanced. */
|
||||
.health-indicator.is-half { flex: 1 1 50%; }
|
||||
|
||||
/* Auto-approve chip — red border + red text, the persistent reminder that
|
||||
this workspace currently has active auto-approve rules. Clicking takes
|
||||
the user to Security > 自动批准策略 so they can review or revoke. */
|
||||
/*
|
||||
Auto-approve chip — persistent indicator that this workspace currently has
|
||||
at least one active auto-approve rule. Designed to be informative, not
|
||||
alarming: a soft danger-tinted pill with a steady pulse on the dot, sized
|
||||
to fit beside the doctor indicator on one row. Colors come from the
|
||||
mateclaw token system (`var(--mc-danger-*)`), so dark mode picks up the
|
||||
appropriate dim variants automatically.
|
||||
*/
|
||||
.auto-approve-chip {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
width: 100%; padding: 6px 10px;
|
||||
border: 1px solid #ef4444;
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1 1 50%;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--mc-danger-border, rgba(192, 57, 43, 0.4));
|
||||
background: var(--mc-danger-bg, rgba(192, 57, 43, 0.12));
|
||||
color: var(--mc-danger, #C0392B);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.2;
|
||||
transition: background-color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.auto-approve-chip:hover {
|
||||
background: var(--mc-danger-bg, rgba(192, 57, 43, 0.18));
|
||||
border-color: var(--mc-danger, #C0392B);
|
||||
}
|
||||
.auto-approve-chip__label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Small live-status dot that gently pulses to suggest "this is active right
|
||||
now" without being aggressive about it. The animation pauses when the user
|
||||
prefers reduced motion. */
|
||||
.auto-approve-chip__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--mc-danger, #C0392B);
|
||||
box-shadow: 0 0 0 0 var(--mc-danger, #C0392B);
|
||||
animation: auto-approve-pulse 2.4s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@keyframes auto-approve-pulse {
|
||||
0% { box-shadow: 0 0 0 0 var(--mc-danger, #C0392B); opacity: 1; }
|
||||
60% { box-shadow: 0 0 0 6px transparent; opacity: 0.6; }
|
||||
100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.auto-approve-chip__dot { animation: none; }
|
||||
}
|
||||
.auto-approve-chip:hover { background: #fee2e2; }
|
||||
.health-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.health-indicator.healthy .health-dot { background: var(--mc-success); }
|
||||
.health-indicator.warning .health-dot { background: var(--mc-primary); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user