feat: animated border on active FAB and grayscale inactive FABs

Active terminal FAB shows a rotating conic-gradient border animation
(cyan/indigo) on all three locations: main FAB (T1), AgentBadge, and
mini FABs (T2-T5). Inactive FABs appear in grayscale with reduced
brightness, brightening slightly on hover.
This commit is contained in:
2026-02-21 14:59:47 -06:00
parent ba4a1a0059
commit 6dc0c5ff6f
4 changed files with 91 additions and 18 deletions

View File

@@ -85,6 +85,13 @@ const keyboardVisible = ref(false) // Virtual keyboard visible
// Whether any terminal exists (T1+)
const hasTerminals = computed(() => sessionState.terminalRegistry.length > 0)
// Whether terminal 1 is the currently active terminal
const isT1Active = computed(() => {
const reg = sessionState.terminalRegistry
if (!reg.length) return false
return reg[0].transcriptSessionId === transcriptDebugRef.value?.activeTerminalSessionId
})
// Extra terminals (T2-T5) from Pinia store — fully reactive, no template ref dependency
const extraTerminals = computed(() => {
const reg = sessionState.terminalRegistry
@@ -366,7 +373,7 @@ watch(() => route.name, (newPage) => {
<span class="fab-bubble b3"></span>
<button
class="transcript-fab"
:class="{ active: showTranscriptDebug }"
:class="{ active: showTranscriptDebug, 't1-active': isT1Active }"
@click="handleMainFabClick"
@contextmenu.prevent
title="Transcript Debug"
@@ -845,8 +852,8 @@ watch(() => route.name, (newPage) => {
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.5),
0 6px 16px rgba(0, 0, 0, 0.6),
0 12px 28px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(14, 165, 233, 0.12);
0 12px 28px rgba(0, 0, 0, 0.4);
filter: grayscale(1) brightness(0.6);
transition: all 0.2s ease;
-webkit-user-select: none;
user-select: none;
@@ -867,15 +874,14 @@ watch(() => route.name, (newPage) => {
pointer-events: none;
}
.transcript-fab:hover {
.transcript-fab:not(.t1-active):hover {
transform: translateY(-2px);
border-color: rgba(14, 165, 233, 0.35);
filter: grayscale(0.5) brightness(0.8);
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.5),
0 10px 24px rgba(0, 0, 0, 0.6),
0 16px 36px rgba(0, 0, 0, 0.4),
0 0 14px rgba(14, 165, 233, 0.15),
inset 0 1px 0 rgba(14, 165, 233, 0.2);
0 0 14px rgba(14, 165, 233, 0.15);
color: #38bdf8;
}
@@ -888,6 +894,31 @@ watch(() => route.name, (newPage) => {
color: #a5f3fc;
}
.transcript-fab.t1-active {
border: 2px solid;
border-image: conic-gradient(
from var(--border-angle, 0deg),
rgba(34, 211, 238, 1),
rgba(99, 102, 241, 0.7),
rgba(34, 211, 238, 0.15),
rgba(99, 102, 241, 0.7),
rgba(34, 211, 238, 1)
) 1;
filter: none;
box-shadow: 0 0 12px rgba(34, 211, 238, 0.3);
animation: border-spin 3s linear infinite;
}
@property --border-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes border-spin {
to { --border-angle: 360deg; }
}
.fab-button-area {
position: relative;
width: 44px;

View File

@@ -625,7 +625,7 @@ onBeforeUnmount(() => {
<AgentBadge
v-if="selectedAgent"
:agent="selectedAgent"
:connected="isRealtime"
:connected="!!activeTerminalSessionId"
:terminals="openTerminals"
:active-session-id="activeTerminalSessionId"
:model="conversation?.model"

View File

@@ -147,14 +147,32 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
}
.agent-badge-wrapper.connected {
border-color: rgba(34, 197, 94, 0.35);
border: 2px solid;
border-image: conic-gradient(
from var(--border-angle, 0deg),
rgba(34, 211, 238, 1),
rgba(99, 102, 241, 0.7),
rgba(34, 211, 238, 0.15),
rgba(99, 102, 241, 0.7),
rgba(34, 211, 238, 1)
) 1;
background: rgba(34, 197, 94, 0.08);
box-shadow: 0 0 8px rgba(34, 197, 94, 0.15);
box-shadow: 0 0 12px rgba(34, 211, 238, 0.3);
animation: border-spin 3s linear infinite;
}
.agent-badge-wrapper.connected:hover {
background: rgba(34, 197, 94, 0.15);
border-color: rgba(34, 197, 94, 0.5);
}
@property --border-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes border-spin {
to { --border-angle: 360deg; }
}
.agent-label {

View File

@@ -86,27 +86,51 @@ const artVariants = [
pointer-events: auto;
background-size: cover;
background-repeat: no-repeat;
filter: grayscale(1) brightness(0.6);
}
.terminal-fab:hover {
.terminal-fab:not(.active):hover {
transform: translateY(-2px);
border-color: rgba(14, 165, 233, 0.35);
filter: grayscale(0.5) brightness(0.8);
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.5),
0 10px 24px rgba(0, 0, 0, 0.6),
0 0 14px rgba(14, 165, 233, 0.15),
inset 0 1px 0 rgba(14, 165, 233, 0.2);
0 0 14px rgba(14, 165, 233, 0.15);
}
.terminal-fab:active {
transform: scale(0.95);
}
.terminal-fab:focus,
.terminal-fab:focus-visible {
outline: none;
}
.terminal-fab.active {
border-color: rgba(34, 211, 238, 0.4);
box-shadow:
0 0 6px rgba(34, 211, 238, 0.15),
inset 0 0 4px rgba(34, 211, 238, 0.08);
border: 2px solid;
border-image: conic-gradient(
from var(--border-angle, 0deg),
rgba(34, 211, 238, 1),
rgba(99, 102, 241, 0.7),
rgba(34, 211, 238, 0.15),
rgba(99, 102, 241, 0.7),
rgba(34, 211, 238, 1)
) 1;
filter: none;
box-shadow: 0 0 12px rgba(34, 211, 238, 0.3);
animation: border-spin 3s linear infinite;
}
@property --border-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes border-spin {
to { --border-angle: 360deg; }
}
.fab-number {