Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/web-ui/src/app/components/SceneBar/SceneBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
}

.bitfun-scene-tab {
// Measured by SceneTab and overridden per-tab; 0 until first measurement.
--bitfun-scene-tab-close-shift: 0px;
position: relative;
display: flex;
align-items: center;
Expand Down Expand Up @@ -136,6 +138,15 @@
&:active {
transform: scale(0.985);
background: var(--bf-appearance-token-element-bg-medium);

// Counter the whole-tab scale drift for the absolutely-positioned close
// button so press/release stays under the cursor. The drift (distance
// from the tab center × 0.015) is measured by SceneTab and exposed as
// --bitfun-scene-tab-close-shift. The transform transition below keeps
// this in sync with the tab's 120ms scale.
.bitfun-scene-tab__close {
transform: translateY(-50%) translateX(var(--bitfun-scene-tab-close-shift));
}
}

&:focus-visible {
Expand All @@ -152,6 +163,12 @@
&:active {
transform: none;
background: transparent;

// No tab-scale drift here (transform is none), so keep the close
// button at its base position instead of applying the compensation.
.bitfun-scene-tab__close {
transform: translateY(-50%);
}
}
}

Expand Down Expand Up @@ -237,9 +254,12 @@
transform: translateY(-50%);
cursor: pointer;
flex-shrink: 0;
transition: opacity $motion-fast $easing-standard,
color $motion-fast $easing-standard,
background $motion-fast $easing-standard;
transition:
// Sync the scale-drift compensation with the tab's 120ms press scale.
transform 120ms cubic-bezier(0.23, 1, 0.32, 1),
opacity $motion-fast $easing-standard,
color $motion-fast $easing-standard,
background $motion-fast $easing-standard;

&:hover {
color: var(--bf-appearance-token-color-accent-500);
Expand Down Expand Up @@ -278,6 +298,13 @@
background: var(--bf-appearance-token-element-bg-medium);
opacity: 1;
}

// Keep the close button at its hovered position while pressed: the global
// motion baseline drops buttons 1px on :active.
.bitfun-scene-tab__close:active {
translate: 0 -1px;
scale: 0.985;
}
}

[data-bf-appearance='bitfun-slate'] .bitfun-scene-tab__icon {
Expand Down Expand Up @@ -349,4 +376,10 @@
transition: none;
transform: none;
}

// No tab-scale drift under reduced motion either (transform is none), so
// keep the close button at its base position instead of the compensation.
.bitfun-scene-tab:active .bitfun-scene-tab__close {
transform: translateY(-50%);
}
}
23 changes: 22 additions & 1 deletion src/web-ui/src/app/components/SceneBar/SceneTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Optional action (e.g. new session) shown inside __content when onActionClick is provided.
*/

import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Plus, X } from 'lucide-react';
import { Tooltip } from '@/component-library';
import type { SceneTab as SceneTabType, SceneTabDef } from './types';
Expand Down Expand Up @@ -36,6 +36,26 @@ const SceneTab: React.FC<SceneTabProps> = ({
onClose,
}) => {
const { Icon, label, pinned } = def;
const tabRef = useRef<HTMLDivElement>(null);

// Expose the close button's scale drift as a CSS var so SceneBar.scss can
// counter it while the tab's 120ms press scale is playing. The close button
// is absolutely positioned (right: 6px, 18px wide → center 15px from the
// right edge); scaling the whole tab 0.985 around its center drifts that
// point left by 0.015 × (width/2 − 15) px, which would make the mouseup
// miss the button. Without this the click bubbles to the tab and activates
// it instead of closing.
useEffect(() => {
const el = tabRef.current;
if (!el) return;
const updateShift = () => {
el.style.setProperty('--bitfun-scene-tab-close-shift', `${0.015 * (el.offsetWidth / 2 - 15)}px`);
};
updateShift();
const observer = new ResizeObserver(updateShift);
observer.observe(el);
return () => observer.disconnect();
}, []);

const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
Expand Down Expand Up @@ -80,6 +100,7 @@ const SceneTab: React.FC<SceneTabProps> = ({

return (
<div
ref={tabRef}
role="tab"
aria-selected={isActive}
tabIndex={isActive ? 0 : -1}
Expand Down