@sleep2agi/agent-network-dashboard 0.4.5-preview.0 → 0.4.5-preview.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +30 -30
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +12 -12
- package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page.js.nft.json +1 -1
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +14 -14
- package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +14 -14
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +7 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +14 -14
- package/.next/server/app/login.segments/_full.segment.rsc +14 -14
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +7 -7
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- package/.next/server/app/logs/page.js.nft.json +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +14 -14
- package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- package/.next/server/app/messages/page.js.nft.json +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +14 -14
- package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- package/.next/server/app/node/page.js.nft.json +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +14 -14
- package/.next/server/app/node.segments/_full.segment.rsc +14 -14
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +7 -7
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- package/.next/server/app/nodes/page.js.nft.json +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +14 -14
- package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs/page.js.nft.json +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +14 -14
- package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page.js.nft.json +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +14 -14
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +14 -14
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +14 -14
- package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page.js.nft.json +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +14 -14
- package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0a0ffmv._.js.map +1 -0
- package/.next/server/chunks/ssr/app_components_08ys5y3._.js +9 -0
- package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -0
- package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +1 -1
- package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +1 -1
- package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
- package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{0hyhubmh-__wg.js → 0b1ymo-btjfha.js} +1 -1
- package/.next/static/chunks/0qgc758gz3on4.js +7 -0
- package/.next/static/chunks/0xxk2ms76kkuz.css +2 -0
- package/.next/static/chunks/1031v6nw33ud3.js +1 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/AppShell.tsx +9 -1
- package/app/components/CommandPalette.tsx +358 -0
- package/app/components/HealthBanner.tsx +132 -0
- package/app/components/HelpOverlay.tsx +121 -0
- package/app/components/LoadingSkeleton.tsx +81 -25
- package/app/components/Sidebar.tsx +18 -0
- package/app/components/UserBar.tsx +16 -2
- package/app/globals.css +181 -0
- package/app/page.tsx +87 -14
- package/app/tasks/page.tsx +35 -5
- package/docs/loop-design-rounds.md +298 -3
- package/docs/screenshots/0.4.5-preview.1/cyber-admin.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/cyber-cmdk-search.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/cyber-cmdk.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/cyber-login.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/cyber-overview.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/cyber-settings.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/cyber-tasks.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/light-admin.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/light-cmdk-search.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/light-cmdk.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/light-login.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/light-overview.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/light-settings.png +0 -0
- package/docs/screenshots/0.4.5-preview.1/light-tasks.png +0 -0
- package/package.json +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__013i9e9._.js +0 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__013i9e9._.js.map +0 -1
- package/.next/static/chunks/0snd~q2ucn.h1.css +0 -2
- package/.next/static/chunks/0xn9.0ejo1s_a.js +0 -7
- package/.next/static/chunks/0y0lo-~k2c~00.js +0 -1
- /package/.next/static/{2m1yIfZCIYDWMYrrJEKSC → dvjBKNMRovte6RFP_k8Lq}/_buildManifest.js +0 -0
- /package/.next/static/{2m1yIfZCIYDWMYrrJEKSC → dvjBKNMRovte6RFP_k8Lq}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{2m1yIfZCIYDWMYrrJEKSC → dvjBKNMRovte6RFP_k8Lq}/_ssgManifest.js +0 -0
|
@@ -1,42 +1,86 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Loading skeleton — mirrors the actual Overview layout structure so the
|
|
5
|
+
* page doesn't appear to "shift" once data arrives. Uses the same
|
|
6
|
+
* `anet-skeleton-pulse` rhythm as the brand pulse (1.6s opacity drift,
|
|
7
|
+
* no scale, no blur, no glow) so the loading animation feels native to
|
|
8
|
+
* the rest of the dashboard rather than a generic spinner.
|
|
9
|
+
*
|
|
10
|
+
* Bars are theme-aware: anet-skeleton-bar (existing CSS shim resolves this to
|
|
11
|
+
* var(--bg-elevated) on light/mint), so light theme shows soft grey blocks
|
|
12
|
+
* on white cards and dark theme shows lighter blocks on navy cards.
|
|
13
|
+
*/
|
|
3
14
|
export function LoadingSkeleton() {
|
|
4
15
|
return (
|
|
5
|
-
<div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-6 font-mono
|
|
6
|
-
{/*
|
|
7
|
-
<div className="
|
|
8
|
-
|
|
9
|
-
<div className="
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
<div className="min-h-screen bg-[#0a0a1a] text-gray-100 p-4 sm:p-6 font-mono">
|
|
17
|
+
{/* KPI top strip — 4 cards matching StatsBar */}
|
|
18
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8 anet-skeleton-pulse">
|
|
19
|
+
{[1, 2, 3, 4].map(i => (
|
|
20
|
+
<div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] px-4 py-3">
|
|
21
|
+
<Bar w="2.5rem" h="1.75rem" />
|
|
22
|
+
<Bar w="3.5rem" h="0.75rem" className="mt-2" />
|
|
23
|
+
<Bar w="5rem" h="0.625rem" className="mt-1" />
|
|
24
|
+
</div>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{/* Dispatch + UserBar row */}
|
|
29
|
+
<div className="flex items-center gap-3 mb-3 anet-skeleton-pulse">
|
|
30
|
+
<Bar w="6rem" h="2.5rem" rounded="0.75rem" />
|
|
31
|
+
<div className="flex-1 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-2.5 flex items-center gap-3">
|
|
32
|
+
<div className="w-8 h-8 rounded-full anet-skeleton-bar" />
|
|
33
|
+
<div className="flex-1">
|
|
34
|
+
<Bar w="6rem" h="0.875rem" />
|
|
35
|
+
<Bar w="10rem" h="0.625rem" className="mt-1" />
|
|
36
|
+
</div>
|
|
16
37
|
</div>
|
|
17
38
|
</div>
|
|
18
39
|
|
|
19
|
-
{/*
|
|
20
|
-
<div className="mb-6
|
|
21
|
-
<
|
|
22
|
-
|
|
40
|
+
{/* Config bar */}
|
|
41
|
+
<div className="mb-6 rounded-lg border border-[#2a2a4a] bg-[#111128] px-4 py-3 anet-skeleton-pulse">
|
|
42
|
+
<Bar w="14rem" h="0.875rem" />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{/* Stat strip 3 cards */}
|
|
46
|
+
<div className="grid grid-cols-3 gap-2 sm:gap-3 mb-3 anet-skeleton-pulse">
|
|
47
|
+
{[1, 2, 3].map(i => (
|
|
48
|
+
<div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-3">
|
|
49
|
+
<Bar w="2rem" h="1.25rem" />
|
|
50
|
+
<Bar w="2.5rem" h="0.75rem" className="mt-1" />
|
|
51
|
+
<Bar w="3.5rem" h="0.625rem" className="mt-px" />
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Nav rail 3 cards */}
|
|
57
|
+
<div className="grid grid-cols-3 gap-2 sm:gap-3 mb-6 anet-skeleton-pulse">
|
|
58
|
+
{[1, 2, 3].map(i => (
|
|
59
|
+
<div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] px-3 py-2.5 flex items-center justify-center gap-2">
|
|
60
|
+
<div className="w-4 h-4 rounded anet-skeleton-bar" />
|
|
61
|
+
<Bar w="4rem" h="0.75rem" />
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
23
64
|
</div>
|
|
24
65
|
|
|
25
|
-
{/*
|
|
26
|
-
<div className="
|
|
66
|
+
{/* Broadcast bar */}
|
|
67
|
+
<div className="mb-6 flex gap-2 anet-skeleton-pulse">
|
|
68
|
+
<div className="flex-1 h-10 rounded-lg border border-[#2a2a4a] bg-[#111128]" />
|
|
69
|
+
<div className="w-28 h-10 rounded-lg anet-skeleton-bar" />
|
|
70
|
+
</div>
|
|
27
71
|
|
|
28
|
-
{/*
|
|
29
|
-
<div className="grid grid-cols-1
|
|
72
|
+
{/* Agent card grid */}
|
|
73
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 sm:gap-4 anet-skeleton-pulse">
|
|
30
74
|
{[1, 2, 3, 4].map(i => (
|
|
31
|
-
<div key={i} className="rounded-xl border border-
|
|
75
|
+
<div key={i} className="rounded-xl border border-[#2a2a4a] bg-[#111128] p-4">
|
|
32
76
|
<div className="flex items-center gap-2 mb-3">
|
|
33
|
-
<div className="w-2
|
|
34
|
-
<
|
|
77
|
+
<div className="w-2 h-2 rounded-full anet-skeleton-bar" />
|
|
78
|
+
<Bar w="6rem" h="0.875rem" />
|
|
35
79
|
</div>
|
|
36
80
|
<div className="space-y-2">
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
<
|
|
81
|
+
<Bar w="100%" h="0.625rem" />
|
|
82
|
+
<Bar w="75%" h="0.625rem" />
|
|
83
|
+
<Bar w="55%" h="0.625rem" />
|
|
40
84
|
</div>
|
|
41
85
|
</div>
|
|
42
86
|
))}
|
|
@@ -45,6 +89,18 @@ export function LoadingSkeleton() {
|
|
|
45
89
|
);
|
|
46
90
|
}
|
|
47
91
|
|
|
92
|
+
/** Single shimmer bar — uses `anet-skeleton-bar` so theme-specific bar
|
|
93
|
+
* color (dark navy on dark themes, mid-grey on light) overrides the
|
|
94
|
+
* default. */
|
|
95
|
+
function Bar({ w, h, rounded, className }: { w: string; h: string; rounded?: string; className?: string }) {
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
className={`anet-skeleton-bar ${className || ''}`}
|
|
99
|
+
style={{ width: w, height: h, borderRadius: rounded || '0.375rem' }}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
48
104
|
// EmptyState export removed in 0.4.5 — replaced by EmptyState.tsx with
|
|
49
105
|
// per-variant glyphs and a NodesEmptyState wrapper for the Overview
|
|
50
106
|
// hint-aware behavior. See app/components/EmptyState.tsx.
|
|
@@ -151,6 +151,24 @@ export function Sidebar() {
|
|
|
151
151
|
Sign out
|
|
152
152
|
</button>
|
|
153
153
|
)}
|
|
154
|
+
{!collapsed && (
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => {
|
|
157
|
+
// Dispatch a synthetic keypress so the global Cmd+K listener fires
|
|
158
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
|
|
159
|
+
}}
|
|
160
|
+
className="w-full flex items-center justify-between gap-2 px-5 py-2 text-[11px] text-gray-600 hover:text-gray-400 hover:bg-[#1a1a2a] transition-colors"
|
|
161
|
+
aria-label="Open command palette"
|
|
162
|
+
>
|
|
163
|
+
<span className="flex items-center gap-2">
|
|
164
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
165
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
166
|
+
</svg>
|
|
167
|
+
Quick search
|
|
168
|
+
</span>
|
|
169
|
+
<kbd className="text-[10px] border border-current rounded px-1 py-0.5 opacity-60 font-mono">⌘K</kbd>
|
|
170
|
+
</button>
|
|
171
|
+
)}
|
|
154
172
|
<div className={`flex items-center gap-2 px-3 py-2 ${collapsed ? 'justify-center' : 'justify-between'}`}>
|
|
155
173
|
<ThemeSwitcher compact={collapsed} />
|
|
156
174
|
<button
|
|
@@ -140,8 +140,22 @@ export function UserBar() {
|
|
|
140
140
|
))}
|
|
141
141
|
</select>
|
|
142
142
|
)}
|
|
143
|
-
<button onClick={() => { setEditName(auth.user?.display_name || ''); setEditEmail(''); setEditing(!editing); }}
|
|
144
|
-
|
|
143
|
+
<button onClick={() => { setEditName(auth.user?.display_name || ''); setEditEmail(''); setEditing(!editing); }}
|
|
144
|
+
className="text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap"
|
|
145
|
+
aria-label="Edit profile">
|
|
146
|
+
<span className="hidden sm:inline">Edit</span>
|
|
147
|
+
<svg className="sm:hidden w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
|
|
148
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.862 4.487z" />
|
|
149
|
+
</svg>
|
|
150
|
+
</button>
|
|
151
|
+
<button onClick={logout}
|
|
152
|
+
className="text-xs text-gray-500 hover:text-gray-300 whitespace-nowrap"
|
|
153
|
+
aria-label="Sign out">
|
|
154
|
+
<span className="hidden sm:inline">Sign out</span>
|
|
155
|
+
<svg className="sm:hidden w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
|
|
156
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
|
157
|
+
</svg>
|
|
158
|
+
</button>
|
|
145
159
|
</div>
|
|
146
160
|
{editing && (
|
|
147
161
|
<div className="flex flex-wrap items-center gap-2 mt-2 pt-2 border-t border-[#2a2a4a]">
|
package/app/globals.css
CHANGED
|
@@ -309,6 +309,66 @@ body {
|
|
|
309
309
|
[data-theme="light"] .text-indigo-400 {
|
|
310
310
|
color: var(--accent) !important;
|
|
311
311
|
}
|
|
312
|
+
|
|
313
|
+
/* Light theme: every "-300/400" accent text color needs a darker mid-tone
|
|
314
|
+
variant. The default Tailwind `-300/-400` shades are designed for dark
|
|
315
|
+
backgrounds — putting them on white gives "barely visible" text (the
|
|
316
|
+
problem Vincent flagged: "浅色下很多字体看不见"). Map each family to its
|
|
317
|
+
`-700` equivalent so contrast against white card is ≥ AA. */
|
|
318
|
+
[data-theme="light"] .text-green-400,
|
|
319
|
+
[data-theme="light"] .text-green-300,
|
|
320
|
+
[data-theme="mint"] .text-green-400,
|
|
321
|
+
[data-theme="mint"] .text-green-300 { color: #047857 !important; }
|
|
322
|
+
|
|
323
|
+
[data-theme="light"] .text-yellow-400,
|
|
324
|
+
[data-theme="light"] .text-yellow-300,
|
|
325
|
+
[data-theme="mint"] .text-yellow-400,
|
|
326
|
+
[data-theme="mint"] .text-yellow-300 { color: #92400e !important; }
|
|
327
|
+
|
|
328
|
+
[data-theme="light"] .text-amber-400,
|
|
329
|
+
[data-theme="light"] .text-amber-300,
|
|
330
|
+
[data-theme="mint"] .text-amber-400,
|
|
331
|
+
[data-theme="mint"] .text-amber-300 { color: #92400e !important; }
|
|
332
|
+
|
|
333
|
+
[data-theme="light"] .text-red-400,
|
|
334
|
+
[data-theme="light"] .text-red-300,
|
|
335
|
+
[data-theme="mint"] .text-red-400,
|
|
336
|
+
[data-theme="mint"] .text-red-300 { color: #b91c1c !important; }
|
|
337
|
+
|
|
338
|
+
[data-theme="light"] .text-purple-400,
|
|
339
|
+
[data-theme="light"] .text-purple-300,
|
|
340
|
+
[data-theme="mint"] .text-purple-400,
|
|
341
|
+
[data-theme="mint"] .text-purple-300 { color: #6d28d9 !important; }
|
|
342
|
+
|
|
343
|
+
[data-theme="light"] .text-orange-400,
|
|
344
|
+
[data-theme="light"] .text-orange-300,
|
|
345
|
+
[data-theme="mint"] .text-orange-400,
|
|
346
|
+
[data-theme="mint"] .text-orange-300 { color: #c2410c !important; }
|
|
347
|
+
|
|
348
|
+
[data-theme="light"] .text-blue-400,
|
|
349
|
+
[data-theme="light"] .text-blue-300,
|
|
350
|
+
[data-theme="mint"] .text-blue-400,
|
|
351
|
+
[data-theme="mint"] .text-blue-300 { color: #1d4ed8 !important; }
|
|
352
|
+
|
|
353
|
+
[data-theme="light"] .text-pink-400,
|
|
354
|
+
[data-theme="light"] .text-pink-300,
|
|
355
|
+
[data-theme="mint"] .text-pink-400,
|
|
356
|
+
[data-theme="mint"] .text-pink-300 { color: #be185d !important; }
|
|
357
|
+
|
|
358
|
+
[data-theme="light"] .text-violet-400,
|
|
359
|
+
[data-theme="light"] .text-violet-300,
|
|
360
|
+
[data-theme="mint"] .text-violet-400,
|
|
361
|
+
[data-theme="mint"] .text-violet-300 { color: #6d28d9 !important; }
|
|
362
|
+
|
|
363
|
+
[data-theme="light"] .text-teal-400,
|
|
364
|
+
[data-theme="light"] .text-teal-300,
|
|
365
|
+
[data-theme="mint"] .text-teal-400,
|
|
366
|
+
[data-theme="mint"] .text-teal-300 { color: #0f766e !important; }
|
|
367
|
+
|
|
368
|
+
/* Light theme: text-gray-700 (used in footers / muted body) is too pale
|
|
369
|
+
when sitting on slightly-grey page bg. Bump to fg-muted. */
|
|
370
|
+
[data-theme="light"] .text-gray-700,
|
|
371
|
+
[data-theme="mint"] .text-gray-700 { color: var(--fg-muted) !important; }
|
|
312
372
|
[data-theme="light"] .bg-emerald-500\/10,
|
|
313
373
|
[data-theme="light"] .bg-emerald-500\/20,
|
|
314
374
|
[data-theme="light"] .bg-cyan-500\/10,
|
|
@@ -539,6 +599,29 @@ body {
|
|
|
539
599
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 0 0 1px var(--border);
|
|
540
600
|
}
|
|
541
601
|
|
|
602
|
+
/* Health banner — sticky thin strip above content, with theme-aware
|
|
603
|
+
green/amber/red tints. Dark themes use the lower-opacity bg+border
|
|
604
|
+
that come from the component classes directly. Light/mint need
|
|
605
|
+
slightly more saturation so the tint reads on a white surface. */
|
|
606
|
+
[data-theme="light"] .anet-health-banner.bg-emerald-500\/6,
|
|
607
|
+
[data-theme="mint"] .anet-health-banner.bg-emerald-500\/6 {
|
|
608
|
+
background-color: rgba(16, 185, 129, 0.08) !important;
|
|
609
|
+
color: #047857 !important;
|
|
610
|
+
border-color: rgba(16, 185, 129, 0.25) !important;
|
|
611
|
+
}
|
|
612
|
+
[data-theme="light"] .anet-health-banner.bg-amber-500\/8,
|
|
613
|
+
[data-theme="mint"] .anet-health-banner.bg-amber-500\/8 {
|
|
614
|
+
background-color: rgba(245, 158, 11, 0.12) !important;
|
|
615
|
+
color: #92400e !important;
|
|
616
|
+
border-color: rgba(245, 158, 11, 0.30) !important;
|
|
617
|
+
}
|
|
618
|
+
[data-theme="light"] .anet-health-banner.bg-red-500\/8,
|
|
619
|
+
[data-theme="mint"] .anet-health-banner.bg-red-500\/8 {
|
|
620
|
+
background-color: rgba(220, 38, 38, 0.10) !important;
|
|
621
|
+
color: #b91c1c !important;
|
|
622
|
+
border-color: rgba(220, 38, 38, 0.30) !important;
|
|
623
|
+
}
|
|
624
|
+
|
|
542
625
|
/* Sidebar brand "online" pulse — slow, restrained, NO glow.
|
|
543
626
|
1.6s opacity drift between 1.0 and 0.5; nothing scales, nothing blurs. */
|
|
544
627
|
@keyframes anet-brand-pulse-kf {
|
|
@@ -552,6 +635,104 @@ body {
|
|
|
552
635
|
.anet-brand-pulse { animation: none; }
|
|
553
636
|
}
|
|
554
637
|
|
|
638
|
+
/* Skeleton pulse — same 1.6s rhythm as the brand pulse but applied to a
|
|
639
|
+
container so all bars inside breathe together. Drifts between full
|
|
640
|
+
token value and 60% so light themes show a calm grey-to-bg-elevated
|
|
641
|
+
fade and dark themes show a calm navy-to-elevated fade. */
|
|
642
|
+
@keyframes anet-skeleton-kf {
|
|
643
|
+
0%, 100% { opacity: 1; }
|
|
644
|
+
50% { opacity: 0.55; }
|
|
645
|
+
}
|
|
646
|
+
.anet-skeleton-pulse {
|
|
647
|
+
animation: anet-skeleton-kf 1.6s ease-in-out infinite;
|
|
648
|
+
}
|
|
649
|
+
@media (prefers-reduced-motion: reduce) {
|
|
650
|
+
.anet-skeleton-pulse { animation: none; }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* Skeleton bar fill — different per theme so it's visible against the
|
|
654
|
+
card bg without being loud. Dark: lighter navy block on dark card.
|
|
655
|
+
Light: mid-grey on white card (not too light or it disappears). */
|
|
656
|
+
.anet-skeleton-bar {
|
|
657
|
+
background-color: #1a1a2a;
|
|
658
|
+
}
|
|
659
|
+
[data-theme="light"] .anet-skeleton-bar,
|
|
660
|
+
[data-theme="mint"] .anet-skeleton-bar {
|
|
661
|
+
background-color: #d4d8df;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/* Command palette modal surface — bumped depth so it sits clearly above
|
|
665
|
+
the page content in both themes. Light/mint: white shell with a real
|
|
666
|
+
shadow (not the dark navy that the CSS shim would normally translate
|
|
667
|
+
`bg-[#0d0d1a]` into). */
|
|
668
|
+
.anet-cmdk kbd {
|
|
669
|
+
font-size: 9px;
|
|
670
|
+
line-height: 1;
|
|
671
|
+
padding: 1px 4px;
|
|
672
|
+
border: 1px solid currentColor;
|
|
673
|
+
border-radius: 3px;
|
|
674
|
+
opacity: 0.6;
|
|
675
|
+
}
|
|
676
|
+
[data-theme="light"] .anet-cmdk > div:not([aria-hidden]),
|
|
677
|
+
[data-theme="mint"] .anet-cmdk > div:not([aria-hidden]) {
|
|
678
|
+
background-color: var(--bg-secondary) !important;
|
|
679
|
+
box-shadow: 0 10px 30px -8px rgba(15, 23, 42, 0.18),
|
|
680
|
+
0 2px 6px -2px rgba(15, 23, 42, 0.10);
|
|
681
|
+
}
|
|
682
|
+
[data-theme="light"] .anet-cmdk-row.bg-cyan-500\/10,
|
|
683
|
+
[data-theme="mint"] .anet-cmdk-row.bg-cyan-500\/10 {
|
|
684
|
+
background-color: rgba(0, 158, 126, 0.08) !important;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* Help overlay — same white-card-with-shadow treatment as cmdk on light */
|
|
688
|
+
[data-theme="light"] .anet-help > div:not([aria-hidden]),
|
|
689
|
+
[data-theme="mint"] .anet-help > div:not([aria-hidden]) {
|
|
690
|
+
background-color: var(--bg-secondary) !important;
|
|
691
|
+
box-shadow: 0 10px 30px -8px rgba(15, 23, 42, 0.18),
|
|
692
|
+
0 2px 6px -2px rgba(15, 23, 42, 0.10);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* Tasks tab strip horizontal scroll fade — gradient overlays at left/right
|
|
696
|
+
edges hint that there's more content when the row overflows. Only
|
|
697
|
+
applied on <sm (where the strip actually scrolls). The wrapper is
|
|
698
|
+
`position: relative` and the fades are absolute siblings. */
|
|
699
|
+
.anet-tabstrip-wrap {
|
|
700
|
+
position: relative;
|
|
701
|
+
}
|
|
702
|
+
.anet-tabstrip-wrap::before,
|
|
703
|
+
.anet-tabstrip-wrap::after {
|
|
704
|
+
content: '';
|
|
705
|
+
position: absolute;
|
|
706
|
+
top: 4px;
|
|
707
|
+
bottom: 4px;
|
|
708
|
+
width: 20px;
|
|
709
|
+
pointer-events: none;
|
|
710
|
+
z-index: 1;
|
|
711
|
+
}
|
|
712
|
+
.anet-tabstrip-wrap::before {
|
|
713
|
+
left: 0;
|
|
714
|
+
background: linear-gradient(to right, #111128, transparent);
|
|
715
|
+
border-radius: 8px 0 0 8px;
|
|
716
|
+
}
|
|
717
|
+
.anet-tabstrip-wrap::after {
|
|
718
|
+
right: 0;
|
|
719
|
+
background: linear-gradient(to left, #111128, transparent);
|
|
720
|
+
border-radius: 0 8px 8px 0;
|
|
721
|
+
}
|
|
722
|
+
@media (min-width: 640px) {
|
|
723
|
+
/* sm: stops scrolling, hide the fades */
|
|
724
|
+
.anet-tabstrip-wrap::before,
|
|
725
|
+
.anet-tabstrip-wrap::after { display: none; }
|
|
726
|
+
}
|
|
727
|
+
[data-theme="light"] .anet-tabstrip-wrap::before,
|
|
728
|
+
[data-theme="mint"] .anet-tabstrip-wrap::before {
|
|
729
|
+
background: linear-gradient(to right, var(--bg-secondary), transparent);
|
|
730
|
+
}
|
|
731
|
+
[data-theme="light"] .anet-tabstrip-wrap::after,
|
|
732
|
+
[data-theme="mint"] .anet-tabstrip-wrap::after {
|
|
733
|
+
background: linear-gradient(to left, var(--bg-secondary), transparent);
|
|
734
|
+
}
|
|
735
|
+
|
|
555
736
|
/* P0-3 baseline: error / warning chip colors for light themes.
|
|
556
737
|
The component-level copy (replacing raw JSON with human text) is fixed
|
|
557
738
|
in the Logs / Server-Logs page components separately. */
|
package/app/page.tsx
CHANGED
|
@@ -193,20 +193,93 @@ export default function Dashboard() {
|
|
|
193
193
|
("0/0", "--", "0") with placeholder arrows ("→ Messages").
|
|
194
194
|
Users couldn't tell which were stats vs which were navigation. */}
|
|
195
195
|
<section className="mb-3 grid grid-cols-3 gap-2 sm:gap-3">
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
196
|
+
{(() => {
|
|
197
|
+
// Build breakdown popover content per card. Pure data — pure CSS
|
|
198
|
+
// popover (no JS state) wires the hover-show transition below.
|
|
199
|
+
const idleCount = sessions.filter(s => isOnline(s) && s.status !== 'working').length;
|
|
200
|
+
const offlineCount = total - online;
|
|
201
|
+
const orderedStatuses = ['running', 'replied', 'failed', 'cancelled', 'expired', 'closed', 'created', 'delivered', 'acked'];
|
|
202
|
+
const failedRecent = tasks.filter((t: { status: string }) => t.status === 'failed').length;
|
|
203
|
+
|
|
204
|
+
const cards = [
|
|
205
|
+
{
|
|
206
|
+
href: '/nodes', label: 'Nodes',
|
|
207
|
+
value: `${online}/${total}`,
|
|
208
|
+
sub: `${total > 0 ? Math.round((online / total) * 100) : 0}% online`,
|
|
209
|
+
color: 'text-green-400 border-green-500/20',
|
|
210
|
+
popover: total === 0 ? null : [
|
|
211
|
+
{ k: 'working', v: working, dot: '', color: '#4ade80' },
|
|
212
|
+
{ k: 'idle', v: idleCount, dot: '', color: '#22d3ee' },
|
|
213
|
+
{ k: 'offline', v: offlineCount, dot: '', color: '#9ca3af' },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
href: '/tasks', label: 'Tasks',
|
|
218
|
+
value: String(Object.values(taskStats).reduce((a, b) => a + b, 0) || 0),
|
|
219
|
+
sub: 'all-time',
|
|
220
|
+
color: 'text-cyan-400 border-cyan-500/20',
|
|
221
|
+
popover: Object.keys(taskStats).length === 0 ? null
|
|
222
|
+
: orderedStatuses
|
|
223
|
+
.filter(s => taskStats[s])
|
|
224
|
+
.map(s => {
|
|
225
|
+
// Inline hex avoids Tailwind purging dynamic
|
|
226
|
+
// `bg-${color}-400` class names.
|
|
227
|
+
const hex = ({
|
|
228
|
+
running: '#4ade80', replied: '#a78bfa', failed: '#f87171',
|
|
229
|
+
cancelled: '#facc15', expired: '#fb923c', closed: '#9ca3af',
|
|
230
|
+
created: '#9ca3af', delivered: '#60a5fa', acked: '#22d3ee',
|
|
231
|
+
} as Record<string, string>)[s] || '#9ca3af';
|
|
232
|
+
return { k: s, v: taskStats[s], dot: '', color: hex };
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
href: '/tasks?status=failed', label: 'Failed',
|
|
237
|
+
value: String(taskStats['failed'] || 0),
|
|
238
|
+
sub: taskStats['failed'] ? 'needs review' : 'none',
|
|
239
|
+
color: taskStats['failed'] ? 'text-red-400 border-red-500/25' : 'text-gray-500 border-gray-700/30',
|
|
240
|
+
popover: !failedRecent ? [{ k: 'no failures yet', v: '', dot: '', color: '#4b5563' }]
|
|
241
|
+
: [{ k: `${failedRecent} in current view`, v: '', dot: '', color: '#f87171' }],
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
return cards.map(a => (
|
|
246
|
+
<Link
|
|
247
|
+
key={a.href}
|
|
248
|
+
href={a.href}
|
|
249
|
+
prefetch={false}
|
|
250
|
+
className={`anet-stat-link group relative rounded-xl border ${a.color} bg-[#111128] px-3 py-3 transition-all hover:-translate-y-px`}
|
|
251
|
+
>
|
|
252
|
+
<div className="flex items-baseline justify-between">
|
|
253
|
+
<div className={`text-xl font-semibold tabular-nums ${a.color.split(' ')[0]}`}>{a.value}</div>
|
|
254
|
+
<div className="hidden sm:block text-[10px] text-gray-600 group-hover:text-gray-400 transition-colors">View →</div>
|
|
255
|
+
</div>
|
|
256
|
+
<div className="text-[11px] text-gray-400 mt-0.5">{a.label}</div>
|
|
257
|
+
<div className="text-[10px] text-gray-600 mt-px">{a.sub}</div>
|
|
258
|
+
|
|
259
|
+
{/* Hover popover — CSS-only, restrained. Hidden on touch
|
|
260
|
+
(no :hover) so mobile is unaffected. Positioned just
|
|
261
|
+
below the card so it doesn't fight nav rail. */}
|
|
262
|
+
{a.popover && a.popover.length > 0 && (
|
|
263
|
+
<div className="anet-stat-popover hidden md:block pointer-events-none absolute left-2 right-2 top-full mt-1 z-20 rounded-lg border border-[#2a2a4a] bg-[#0d0d1a] shadow-lg shadow-black/30 px-3 py-2 opacity-0 translate-y-[-2px] group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-150 delay-100">
|
|
264
|
+
<div className="text-[10px] text-gray-600 uppercase tracking-wider mb-1.5">Breakdown</div>
|
|
265
|
+
<ul className="space-y-1">
|
|
266
|
+
{a.popover.map(row => (
|
|
267
|
+
<li key={row.k} className="flex items-center gap-2 text-[11px]">
|
|
268
|
+
<span
|
|
269
|
+
className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
|
|
270
|
+
style={{ backgroundColor: row.color }}
|
|
271
|
+
aria-hidden
|
|
272
|
+
/>
|
|
273
|
+
<span className="text-gray-400 capitalize">{row.k}</span>
|
|
274
|
+
{row.v !== '' && <span className="ml-auto text-gray-300 tabular-nums font-medium">{row.v}</span>}
|
|
275
|
+
</li>
|
|
276
|
+
))}
|
|
277
|
+
</ul>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</Link>
|
|
281
|
+
));
|
|
282
|
+
})()}
|
|
210
283
|
</section>
|
|
211
284
|
|
|
212
285
|
{/* Nav rail — pure navigation, icon + label, no data */}
|
package/app/tasks/page.tsx
CHANGED
|
@@ -48,6 +48,21 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
48
48
|
expired: 'bg-orange-500/10 text-orange-300 border-orange-500/20',
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/** Small colored dot per status — visible on every tab (active + inactive)
|
|
52
|
+
* so users can scan the row at a glance. Hex inlined to avoid Tailwind
|
|
53
|
+
* purging dynamic `bg-${family}-400` class names. */
|
|
54
|
+
const STATUS_DOTS: Record<string, string> = {
|
|
55
|
+
created: '#9ca3af',
|
|
56
|
+
delivered: '#60a5fa',
|
|
57
|
+
acked: '#22d3ee',
|
|
58
|
+
running: '#4ade80',
|
|
59
|
+
replied: '#a78bfa',
|
|
60
|
+
closed: '#6b7280',
|
|
61
|
+
failed: '#f87171',
|
|
62
|
+
cancelled: '#facc15',
|
|
63
|
+
expired: '#fb923c',
|
|
64
|
+
};
|
|
65
|
+
|
|
51
66
|
function statusBadge(status: string) {
|
|
52
67
|
const color = STATUS_COLORS[status] || 'bg-gray-500/10 text-gray-400 border-gray-500/20';
|
|
53
68
|
return `text-xs px-2 py-0.5 rounded-md border ${color}`;
|
|
@@ -134,19 +149,34 @@ function TasksContent() {
|
|
|
134
149
|
</span>
|
|
135
150
|
</div>
|
|
136
151
|
|
|
137
|
-
{/* Status Tabs
|
|
138
|
-
|
|
152
|
+
{/* Status Tabs — color-coded dots per status family so users can
|
|
153
|
+
scan the row at a glance. Active tab gets full chip styling
|
|
154
|
+
(bg + border + text color), inactive tabs only carry the dot
|
|
155
|
+
+ neutral label so the active state remains the strong cue.
|
|
156
|
+
On narrow viewports (<sm) the row scrolls horizontally with
|
|
157
|
+
subtle gradient fade-edges (via .anet-tabstrip-wrap pseudo
|
|
158
|
+
elements) hinting more content. */}
|
|
159
|
+
<div className="anet-tabstrip-wrap mb-4">
|
|
160
|
+
<div className="anet-tabstrip flex sm:flex-wrap gap-1 bg-[#111128] rounded-lg border border-[#2a2a4a] p-1 overflow-x-auto sm:overflow-x-visible scrollbar-thin">
|
|
139
161
|
{['', ...STATUS_OPTIONS.filter(Boolean)].map(s => (
|
|
140
162
|
<button key={s} onClick={() => setFilterStatus(s)}
|
|
141
|
-
className={`px-3 py-1.5 rounded-md text-xs transition-colors ${
|
|
163
|
+
className={`px-3 py-1.5 rounded-md text-xs transition-colors flex items-center gap-1.5 shrink-0 whitespace-nowrap ${
|
|
142
164
|
filterStatus === s
|
|
143
165
|
? `${STATUS_COLORS[s] || 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'} border`
|
|
144
|
-
: 'text-gray-500 hover:text-gray-300'
|
|
166
|
+
: 'text-gray-500 hover:text-gray-300 hover:bg-[#1a1a2a]/40'
|
|
145
167
|
}`}>
|
|
146
|
-
{s
|
|
168
|
+
{s && (
|
|
169
|
+
<span
|
|
170
|
+
aria-hidden
|
|
171
|
+
className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
|
|
172
|
+
style={{ backgroundColor: STATUS_DOTS[s] || '#6b7280' }}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
<span>{s || 'All'}</span>
|
|
147
176
|
</button>
|
|
148
177
|
))}
|
|
149
178
|
</div>
|
|
179
|
+
</div>
|
|
150
180
|
|
|
151
181
|
{/* From/To Filters */}
|
|
152
182
|
<div className="flex flex-wrap gap-3 mb-6">
|