-
-
- SA
- StoreAI
-
- {{ badgeText }}
-
+
+
-
{{ statusLine() }}
+
+
+ 请先登录后再开始爬取
+
+
-
- 平台:{{ platformConfigs.find((p) => p.id === selectedPlatformId)?.name ?? selectedPlatformId }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ crawlState.platformName }}
+
+ Paused
+ Done
+ Failed
+ Scanning
+ · {{ formatSeconds(elapsedSeconds) }}
+
+
+
+
+
+
+
+
+ ✓
+ ×
+ •
+
+
{{ index + 1 }}. {{ step.name }}
+
+
+ 已完成
+ 失败
+ 进行中
+ 等待中
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ crawlState.platformName }}
-
- {{
- card === 'paused'
- ? 'Paused'
- : card === 'done'
- ? 'Done'
- : card === 'failed'
- ? 'Failed'
- : card === 'cancelled'
- ? 'Cancelled'
- : 'Scanning'
- }}
- · {{ formatSeconds(elapsedSeconds) }}
-
-
-
-
-
-
-
-
{{ dotFor(step.status) }}
-
{{ index + 1 }}. {{ step.name }}
-
-
{{ stepStatusText(step.status) }}
-
-
-
-
-
{{ crawlState.pause.message }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+@import "tailwindcss";
+
\ No newline at end of file
diff --git a/src/popup/hook/use-login.ts b/src/popup/hook/use-login.ts
index 72a1c9e..02cdb3d 100644
--- a/src/popup/hook/use-login.ts
+++ b/src/popup/hook/use-login.ts
@@ -1,35 +1,42 @@
-import {computed, onMounted, ref} from "vue";
-import {getToken, logout, setToken} from "@/shared/auth";
+import { computed, onMounted, ref } from 'vue';
+import { getToken, logout, setToken } from '@/shared/auth';
+/**
+ * Popup 的登录状态与操作。
+ */
export const useLogin = () => {
const token = ref
(null);
+ /** 当前是否已登录。 */
const isLoggedIn = computed(() => token.value !== null);
/**
- * 登录
+ * 登录并保存 token。
*/
const handleLogin = async () => {
- let value = "xxx"
- await setToken(value)
- token.value = value
- }
+ const value = 'xxx';
+ await setToken(value);
+ token.value = value;
+ };
/**
- * 退出登录
+ * 退出登录并清理本地状态。
*/
const handleLogout = async () => {
- await logout()
- token.value = null
- }
+ await logout();
+ token.value = null;
+ };
+ /**
+ * 组件挂载时,从存储恢复 token。
+ */
onMounted(async () => {
- token.value = await getToken()
- })
+ token.value = await getToken();
+ });
return {
isLoggedIn,
handleLogin,
handleLogout,
- }
-}
\ No newline at end of file
+ };
+};
diff --git a/src/popup/hook/use-scan.ts b/src/popup/hook/use-scan.ts
index 20b04e8..3f7a266 100644
--- a/src/popup/hook/use-scan.ts
+++ b/src/popup/hook/use-scan.ts
@@ -1,19 +1,35 @@
-import { onMounted, onUnmounted, ref } from 'vue';
-import { platformConfigs } from '@/config/platforms';
-import type { CrawlTaskState } from '@/types';
-import { sendBackgroundMessage } from '@/shared/message';
+import {computed, onMounted, onUnmounted, ref} from 'vue';
+import {platformConfigs} from '@/config/platforms';
+import type {CrawlTaskState} from '@/types';
+import {sendBackgroundMessage} from '@/shared/message';
+/** 用于同步爬取任务状态的 `chrome.storage.local` key。 */
const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState';
+/** 会持续刷新计时器的任务状态集合。 */
const ACTIVE_STATUSES = new Set(['running', 'paused']);
+/**
+ * Popup 内的爬取状态与操作集合。
+ */
export const useScan = () => {
+ /** 当前选中的平台 id。 */
const selectedPlatformId = ref(platformConfigs[0]?.id ?? '');
+ /** 防止重复点击“Scan now”(打开扫描窗口期间置为 true)。 */
const isScanning = ref(false);
+ /**
+ * 当前爬取任务状态(从 background 同步)。
+ */
const crawlState = ref(null);
+
+ const taskStatus = computed(() => crawlState.value?.status);
+ /**
+ * 从任务开始到现在的秒数。
+ */
const elapsedSeconds = ref(0);
let timer: number | undefined;
+ /** 启动新的爬取任务(使用当前选择的平台)。 */
const handleScan = async () => {
if (isScanning.value) {
return;
@@ -26,7 +42,7 @@ export const useScan = () => {
const response = await sendBackgroundMessage({
action: 'START_CRAWL',
- payload: { platformId: selectedPlatformId.value },
+ payload: {platformId: selectedPlatformId.value},
});
if (response.ok) {
@@ -39,8 +55,9 @@ export const useScan = () => {
}
};
+ /** 通知 background 取消当前任务。 */
const handleCancelCrawl = async () => {
- const response = await sendBackgroundMessage({ action: 'CANCEL_CRAWL' });
+ const response = await sendBackgroundMessage({action: 'CANCEL_CRAWL'});
if (response.ok) {
syncCrawlState(response.data ?? null);
@@ -51,8 +68,9 @@ export const useScan = () => {
await refreshCrawlState();
};
+ /** 通知 background 恢复被暂停的任务。 */
const handleResumeCrawl = async () => {
- const response = await sendBackgroundMessage({ action: 'RESUME_CRAWL' });
+ const response = await sendBackgroundMessage({action: 'RESUME_CRAWL'});
if (response.ok) {
syncCrawlState(response.data ?? null);
@@ -63,8 +81,9 @@ export const useScan = () => {
await refreshCrawlState();
};
+ /** 关闭任务卡片并通知 background 清理任务状态。 */
const handleDismissCrawl = async () => {
- const response = await sendBackgroundMessage({ action: 'DISMISS_CRAWL' });
+ const response = await sendBackgroundMessage({action: 'DISMISS_CRAWL'});
if (response.ok) {
syncCrawlState(response.data ?? null);
@@ -75,6 +94,7 @@ export const useScan = () => {
await refreshCrawlState();
};
+ /** 应用任务状态:刷新 elapsed,并根据状态管理计时器的开启/关闭。 */
function syncCrawlState(state: CrawlTaskState | null) {
crawlState.value = state;
updateSeconds();
@@ -87,6 +107,7 @@ export const useScan = () => {
clearElapsedTimer();
}
+ /** 确保 1 秒一次的计时器正在运行。 */
function ensureElapsedTimer() {
if (timer !== undefined) {
return;
@@ -97,6 +118,7 @@ export const useScan = () => {
}, 1000);
}
+ /** 停止计时器(如果存在)。 */
function clearElapsedTimer() {
if (timer === undefined) {
return;
@@ -106,6 +128,7 @@ export const useScan = () => {
timer = undefined;
}
+ /** 根据任务 `startedAt` 更新时间(秒)。 */
function updateSeconds() {
if (!crawlState.value) {
elapsedSeconds.value = 0;
@@ -115,14 +138,16 @@ export const useScan = () => {
elapsedSeconds.value = Math.max(0, Math.floor((Date.now() - crawlState.value.startedAt) / 1000));
}
+ /** 从 background 拉取最新任务状态。 */
async function refreshCrawlState() {
- const response = await sendBackgroundMessage({ action: 'GET_CRAWL_STATE' });
+ const response = await sendBackgroundMessage({action: 'GET_CRAWL_STATE'});
if (response.ok) {
syncCrawlState(response.data ?? null);
}
}
+ /** 监听 `chrome.storage` 的变化,用于跨上下文同步任务状态。 */
function handleStorageChanged(changes: Record, areaName: string) {
if (areaName !== 'local') {
return;
@@ -138,6 +163,7 @@ export const useScan = () => {
}
onMounted(async () => {
+ /** 首次加载 + 订阅 storage 事件。 */
await refreshCrawlState();
if (typeof chrome !== 'undefined' && chrome.storage?.onChanged) {
@@ -146,6 +172,7 @@ export const useScan = () => {
});
onUnmounted(() => {
+ /** 清理计时器 + 取消订阅 storage 事件。 */
clearElapsedTimer();
if (typeof chrome !== 'undefined' && chrome.storage?.onChanged) {
@@ -157,6 +184,7 @@ export const useScan = () => {
selectedPlatformId,
isScanning,
crawlState,
+ taskStatus,
handleScan,
handleCancelCrawl,
handleResumeCrawl,
@@ -165,6 +193,7 @@ export const useScan = () => {
};
};
+/** storage 数据的运行时类型保护(防御不可信数据)。 */
function isCrawlTaskState(value: unknown): value is CrawlTaskState {
return typeof value === 'object' && value !== null && 'id' in value && 'steps' in value;
}
diff --git a/src/popup/main.ts b/src/popup/main.ts
index ba9cdcf..7b2237e 100644
--- a/src/popup/main.ts
+++ b/src/popup/main.ts
@@ -1,5 +1,5 @@
import { createApp } from 'vue';
import App from './App.vue';
-import './popup.css';
+import './popup.scss';
createApp(App).mount('#app');
diff --git a/src/popup/popup.css b/src/popup/popup.css
deleted file mode 100644
index dfd8e01..0000000
--- a/src/popup/popup.css
+++ /dev/null
@@ -1,392 +0,0 @@
-:root {
- --bg: #ffffff;
- --fg: #0f172a;
- --muted: #64748b;
- --border: #e2e8f0;
- --primary: #0f172a;
- --primary-fg: #ffffff;
- --accent: #f1f5f9;
- --success: #22c55e;
- --warning: #eab308;
- --danger: #ef4444;
-}
-
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-body {
- width: 360px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
- background: var(--bg);
- color: var(--fg);
-}
-
-.container {
- padding: 16px;
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.logo {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- font-size: 15px;
- font-weight: 600;
- letter-spacing: -0.01em;
-}
-.logo-mark {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- background: var(--primary);
- color: var(--primary-fg);
- border-radius: 5px;
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0;
-}
-
-.badge {
- font-size: 10px;
- font-weight: 600;
- padding: 2px 8px;
- border-radius: 999px;
- background: var(--accent);
- color: var(--muted);
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.badge-ok {
- background: rgba(34, 197, 94, 0.12);
- color: var(--success);
-}
-
-.status {
- font-size: 13px;
- color: var(--muted);
- line-height: 1.5;
-}
-
-.account {
- font-size: 12px;
- color: var(--muted);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.last-scan {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 13px;
- padding: 8px 12px;
- background: var(--accent);
- border-radius: 6px;
-}
-
-.dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: var(--muted);
-}
-.dot-green {
- background: var(--success);
-}
-.dot-yellow {
- background: var(--warning);
-}
-.dot-red {
- background: var(--danger);
-}
-
-.progress {
- font-size: 12px;
- color: var(--muted);
- padding: 6px 8px;
- background: var(--accent);
- border-radius: 4px;
- font-style: italic;
-}
-
-button {
- width: 100%;
- padding: 9px 12px;
- border: none;
- border-radius: 6px;
- background: var(--primary);
- color: var(--primary-fg);
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- transition: opacity 150ms ease;
-}
-button:hover:not(:disabled) {
- opacity: 0.9;
-}
-
-button.secondary {
- background: transparent;
- color: var(--muted);
- border: 1px solid var(--border);
-}
-
-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-footer {
- border-top: 1px solid var(--border);
- padding-top: 8px;
- font-size: 11px;
- color: var(--muted);
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
-}
-footer .version {
- flex: 0 0 auto;
-}
-
-/* ===============================================================
- Scanning state - radar card + step list + pause banner
- =============================================================== */
-
-.badge-scanning,
-.badge-starting,
-.badge-drilling,
-.badge-competitors,
-.badge-uploading {
- background: rgba(14, 165, 233, 0.12);
- color: #0ea5e9;
-}
-.badge-paused {
- background: rgba(234, 179, 8, 0.15);
- color: #ca8a04;
-}
-.badge-done {
- background: rgba(34, 197, 94, 0.15);
- color: #16a34a;
-}
-.badge-cancelled {
- background: rgba(100, 116, 139, 0.15);
- color: #64748b;
-}
-.badge-failed {
- background: rgba(239, 68, 68, 0.15);
- color: #dc2626;
-}
-
-.radar-card {
- background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
- color: #e2e8f0;
- border-radius: 10px;
- padding: 14px;
- display: flex;
- flex-direction: column;
- gap: 10px;
-}
-.radar-card.paused {
- background: linear-gradient(180deg, #2b2008 0%, #3d2b0f 100%);
-}
-.radar-card.done {
- background: linear-gradient(180deg, #0a2e1a 0%, #134028 100%);
-}
-.radar-card.failed {
- background: linear-gradient(180deg, #2a0f0f 0%, #3b1718 100%);
-}
-.radar-card.cancelled {
- background: linear-gradient(180deg, #1e293b 0%, #263345 100%);
-}
-
-.radar-row {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-.radar {
- flex: 0 0 40px;
- width: 40px;
- height: 40px;
- position: relative;
- overflow: hidden;
- border-radius: 50%;
- background: radial-gradient(
- circle at center,
- rgba(46, 160, 67, 0.14),
- rgba(46, 160, 67, 0.02) 70%,
- transparent 80%
- );
- border: 1px solid rgba(46, 160, 67, 0.35);
-}
-.radar .sweep {
- position: absolute;
- inset: 0;
- background: conic-gradient(
- from 0deg,
- rgba(46, 160, 67, 0) 0deg,
- rgba(46, 160, 67, 0.75) 50deg,
- rgba(46, 160, 67, 0) 60deg
- );
- animation: pop-sweep 2s linear infinite;
-}
-.radar .ping {
- position: absolute;
- left: 50%;
- top: 50%;
- width: 7px;
- height: 7px;
- background: #2ea043;
- border-radius: 50%;
- transform: translate(-50%, -50%);
- box-shadow: 0 0 0 0 rgba(46, 160, 67, 0.7);
- animation: pop-ping 2s ease-out infinite;
-}
-@keyframes pop-sweep {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-@keyframes pop-ping {
- 0% {
- box-shadow: 0 0 0 0 rgba(46, 160, 67, 0.7);
- }
- 100% {
- box-shadow: 0 0 0 14px rgba(46, 160, 67, 0);
- }
-}
-.radar-card.paused .sweep,
-.radar-card.done .sweep,
-.radar-card.cancelled .sweep,
-.radar-card.failed .sweep {
- animation: none;
- opacity: 0.3;
-}
-.radar-card.paused .ping {
- background: #eab308;
- animation: none;
-}
-.radar-card.done .ping {
- background: #22c55e;
- animation: none;
-}
-.radar-card.failed .ping {
- background: #ef4444;
- animation: none;
-}
-.radar-card.cancelled .ping {
- background: #94a3b8;
- animation: none;
-}
-
-.radar-titles {
- flex: 1 1 auto;
- min-width: 0;
-}
-.radar-title {
- font-size: 13px;
- font-weight: 600;
- line-height: 1.2;
- color: #f8fafc;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.radar-sub {
- font-size: 11.5px;
- color: rgba(226, 232, 240, 0.75);
- margin-top: 2px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.steps {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-.step {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- font-size: 12px;
- color: rgba(226, 232, 240, 0.8);
-}
-.step-left {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- min-width: 0;
- flex: 1 1 auto;
-}
-.step-dot {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: rgba(226, 232, 240, 0.18);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-size: 10px;
- font-weight: 700;
-}
-.step-label {
- min-width: 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.step-status {
- flex: 0 0 auto;
- font-size: 11px;
- opacity: 0.85;
-}
-
-.pause-banner {
- border-radius: 8px;
- border: 1px solid rgba(234, 179, 8, 0.35);
- background: rgba(234, 179, 8, 0.08);
- padding: 10px 12px;
- font-size: 12px;
- line-height: 1.45;
-}
-.pause-banner p {
- margin: 0;
- color: #fef3c7;
-}
-
-.actions {
- display: flex;
- gap: 8px;
-}
-.actions button {
- width: auto;
- flex: 1 1 0;
-}
-.actions button.secondary {
- color: rgba(226, 232, 240, 0.85);
- border-color: rgba(226, 232, 240, 0.2);
-}
-
diff --git a/src/popup/popup.scss b/src/popup/popup.scss
new file mode 100644
index 0000000..b614553
--- /dev/null
+++ b/src/popup/popup.scss
@@ -0,0 +1,410 @@
+:root {
+ --bg: #ffffff;
+ --fg: #0f172a;
+ --muted: #64748b;
+ --border: #e2e8f0;
+ --primary: #0f172a;
+ --primary-fg: #ffffff;
+ --accent: #f1f5f9;
+ --success: #22c55e;
+ --warning: #eab308;
+ --danger: #ef4444;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ width: 360px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
+ background: var(--bg);
+ color: var(--fg);
+}
+
+.container {
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.logo {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 15px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+
+ &-mark {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ background: var(--primary);
+ color: var(--primary-fg);
+ border-radius: 5px;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0;
+ }
+}
+
+
+.status {
+ font-size: 13px;
+ color: var(--muted);
+ line-height: 1.5;
+}
+
+.account {
+ font-size: 12px;
+ color: var(--muted);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.platform-select {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+
+ &__control {
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: #fff;
+ font-size: 13px;
+
+ &:focus {
+ outline: none;
+ }
+ }
+}
+
+.last-scan {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ padding: 8px 12px;
+ background: var(--accent);
+ border-radius: 6px;
+}
+
+.dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--muted);
+
+ &-green {
+ background: var(--success);
+ }
+
+ &-yellow {
+ background: var(--warning);
+ }
+
+ &-red {
+ background: var(--danger);
+ }
+}
+
+.progress {
+ font-size: 12px;
+ color: var(--muted);
+ padding: 6px 8px;
+ background: var(--accent);
+ border-radius: 4px;
+ font-style: italic;
+}
+
+button {
+ width: 100%;
+ padding: 9px 12px;
+ border: none;
+ border-radius: 6px;
+ background: var(--primary);
+ color: var(--primary-fg);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: opacity 150ms ease;
+
+ &:hover:not(:disabled) {
+ opacity: 0.9;
+ }
+
+ &.secondary {
+ background: transparent;
+ color: var(--muted);
+ border: 1px solid var(--border);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+footer {
+ border-top: 1px solid var(--border);
+ padding-top: 8px;
+ font-size: 11px;
+ color: var(--muted);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+
+ .footer-btn {
+ width: auto;
+ padding: 4px 10px;
+ }
+
+ .version {
+ flex: 0 0 auto;
+ }
+}
+
+.radar-card {
+ background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
+ color: #e2e8f0;
+ border-radius: 10px;
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ &.paused {
+ background: linear-gradient(180deg, #2b2008 0%, #3d2b0f 100%);
+ }
+
+ &.done {
+ background: linear-gradient(180deg, #0a2e1a 0%, #134028 100%);
+ }
+
+ &.failed {
+ background: linear-gradient(180deg, #2a0f0f 0%, #3b1718 100%);
+ }
+
+ &.canceled {
+ background: linear-gradient(180deg, #1e293b 0%, #263345 100%);
+ }
+
+ &.paused,
+ &.done,
+ &.canceled,
+ &.failed {
+ .sweep {
+ animation: none;
+ opacity: 0.3;
+ }
+ }
+
+ &.paused {
+ .ping {
+ background: #eab308;
+ animation: none;
+ }
+ }
+
+ &.done {
+ .ping {
+ background: #22c55e;
+ animation: none;
+ }
+ }
+
+ &.failed {
+ .ping {
+ background: #ef4444;
+ animation: none;
+ }
+ }
+
+ &.canceled {
+ .ping {
+ background: #94a3b8;
+ animation: none;
+ }
+ }
+}
+
+.radar-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.radar {
+ flex: 0 0 40px;
+ width: 40px;
+ height: 40px;
+ position: relative;
+ overflow: hidden;
+ border-radius: 50%;
+ background: radial-gradient(
+ circle at center,
+ rgba(46, 160, 67, 0.14),
+ rgba(46, 160, 67, 0.02) 70%,
+ transparent 80%
+ );
+ border: 1px solid rgba(46, 160, 67, 0.35);
+
+ .sweep {
+ position: absolute;
+ inset: 0;
+ background: conic-gradient(
+ from 0deg,
+ rgba(46, 160, 67, 0) 0deg,
+ rgba(46, 160, 67, 0.75) 50deg,
+ rgba(46, 160, 67, 0) 60deg
+ );
+ animation: pop-sweep 2s linear infinite;
+ }
+
+ .ping {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 7px;
+ height: 7px;
+ background: #2ea043;
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ box-shadow: 0 0 0 0 rgba(46, 160, 67, 0.7);
+ animation: pop-ping 2s ease-out infinite;
+ }
+}
+
+@keyframes pop-sweep {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes pop-ping {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(46, 160, 67, 0.7);
+ }
+ 100% {
+ box-shadow: 0 0 0 14px rgba(46, 160, 67, 0);
+ }
+}
+
+.radar-titles {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.radar-title {
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.2;
+ color: #f8fafc;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.radar-sub {
+ font-size: 11.5px;
+ color: rgba(226, 232, 240, 0.75);
+ margin-top: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.steps {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.step {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ font-size: 12px;
+ color: rgba(226, 232, 240, 0.8);
+
+ &-left {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ flex: 1 1 auto;
+ }
+
+ &-dot {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: rgba(226, 232, 240, 0.18);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ font-weight: 700;
+ }
+
+ &-label {
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &-status {
+ flex: 0 0 auto;
+ font-size: 11px;
+ opacity: 0.85;
+ }
+}
+
+.pause-banner {
+ border-radius: 8px;
+ border: 1px solid rgba(234, 179, 8, 0.35);
+ background: rgba(234, 179, 8, 0.08);
+ padding: 10px 12px;
+ font-size: 12px;
+ line-height: 1.45;
+
+ p {
+ margin: 0;
+ color: #fef3c7;
+ }
+}
+
+.actions {
+ display: flex;
+ gap: 8px;
+
+ button {
+ width: auto;
+ flex: 1 1 0;
+
+ &.secondary {
+ color: rgba(226, 232, 240, 0.85);
+ border-color: rgba(226, 232, 240, 0.2);
+ }
+ }
+}