Skip to content

Commit cb2fc47

Browse files
Sync tab breathing across all tabs, add icon-less indicators
Replaced per-tab CSS animations with a single --breathe-phase variable animated on :root via CSS @Property. All breathing tabs read the same phase so they pulse perfectly in sync. Changes: - icon="none" hides the icon but keeps the background glow - Breathing only on active state (icon=none), done states get static bg - 16s ease-in-out cycle for a calm, barely-there pulse - indicator-breathing CSS class separates animated vs static indicators Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2cf3c49 commit cb2fc47

File tree

2 files changed

+43
-25
lines changed

2 files changed

+43
-25
lines changed

frontend/app/tab/tab.scss

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
// Copyright 2024, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
// Animate a shared breathing phase on :root so all indicator tabs pulse in sync.
5+
// Uses CSS @property for an animatable custom property (Chromium 78+).
6+
@property --breathe-phase {
7+
syntax: "<number>";
8+
initial-value: 0;
9+
inherits: true;
10+
}
11+
12+
:root {
13+
animation: indicatorBreathePhase 16s ease-in-out infinite;
14+
}
15+
16+
@keyframes indicatorBreathePhase {
17+
0%,
18+
100% {
19+
--breathe-phase: 0;
20+
}
21+
50% {
22+
--breathe-phase: 1;
23+
}
24+
}
25+
426
.tab {
527
position: absolute;
628
width: 130px;
@@ -98,10 +120,19 @@
98120

99121
&.has-indicator {
100122
.tab-inner {
101-
animation: indicatorBreathe 5s ease-in-out infinite;
123+
background: rgb(from var(--tab-indicator-color) r g b / 0.15);
124+
}
125+
&.active .tab-inner {
126+
background: rgb(from var(--tab-indicator-color) r g b / 0.2);
127+
}
128+
}
129+
130+
&.indicator-breathing {
131+
.tab-inner {
132+
background: rgb(from var(--tab-indicator-color) r g b / calc(0.08 + var(--breathe-phase) * 0.14));
102133
}
103134
&.active .tab-inner {
104-
animation: indicatorBreatheActive 5s ease-in-out infinite;
135+
background: rgb(from var(--tab-indicator-color) r g b / calc(0.12 + var(--breathe-phase) * 0.14));
105136
}
106137
}
107138

@@ -139,10 +170,16 @@ body:not(.nohover) .tab.dragging {
139170
background: rgb(from var(--main-text-color) r g b / 0.1);
140171
}
141172
&.has-indicator .tab-inner {
142-
animation: indicatorBreathe 5s ease-in-out infinite;
173+
background: rgb(from var(--tab-indicator-color) r g b / 0.15);
143174
}
144175
&.has-indicator.active .tab-inner {
145-
animation: indicatorBreatheActive 5s ease-in-out infinite;
176+
background: rgb(from var(--tab-indicator-color) r g b / 0.2);
177+
}
178+
&.indicator-breathing .tab-inner {
179+
background: rgb(from var(--tab-indicator-color) r g b / calc(0.08 + var(--breathe-phase) * 0.14));
180+
}
181+
&.indicator-breathing.active .tab-inner {
182+
background: rgb(from var(--tab-indicator-color) r g b / calc(0.12 + var(--breathe-phase) * 0.14));
146183
}
147184
.close {
148185
visibility: visible;
@@ -157,26 +194,6 @@ body.nohover .tab.active .close {
157194
visibility: visible;
158195
}
159196

160-
@keyframes indicatorBreathe {
161-
0%,
162-
100% {
163-
background: rgb(from var(--tab-indicator-color) r g b / 0.12);
164-
}
165-
50% {
166-
background: rgb(from var(--tab-indicator-color) r g b / 0.18);
167-
}
168-
}
169-
170-
@keyframes indicatorBreatheActive {
171-
0%,
172-
100% {
173-
background: rgb(from var(--tab-indicator-color) r g b / 0.16);
174-
}
175-
50% {
176-
background: rgb(from var(--tab-indicator-color) r g b / 0.22);
177-
}
178-
}
179-
180197
@keyframes expandWidthAndFadeIn {
181198
from {
182199
width: var(--initial-tab-width);

frontend/app/tab/tab.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ const Tab = memo(
225225
"before-active": isBeforeActive,
226226
"new-tab": isNew,
227227
"has-indicator": indicator != null,
228+
"indicator-breathing": indicator != null && indicator.icon === "none",
228229
})}
229230
style={
230231
indicator != null
@@ -248,7 +249,7 @@ const Tab = memo(
248249
>
249250
{tabData?.name}
250251
</div>
251-
{indicator && (
252+
{indicator && indicator.icon !== "none" && (
252253
<div
253254
className="tab-indicator pointer-events-none"
254255
style={{ color: indicator.color || "#fbbf24" }}

0 commit comments

Comments
 (0)