Skip to content
Merged
18 changes: 16 additions & 2 deletions src/web-ui/src/app/layout/WorkspaceBody.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,27 @@ $_nav-collapsed-width: 80px;
background: transparent;

&::after {
content: none;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
border-radius: $size-radius-full;
background: var(--color-accent-500);
opacity: 0;
transform: translateX(-50%);
transition: opacity $motion-fast $easing-standard;
pointer-events: none;
}

&:hover::after {
opacity: 0.55;
}
}

// Active resizing state — keep line visible while dragging
.bitfun-is-resizing-nav .bitfun-workspace-body__nav-divider::after {
content: '';
opacity: 1;
width: 3px;
background: var(--color-accent-500);
Expand Down
16 changes: 1 addition & 15 deletions src/web-ui/src/app/layout/WorkspaceBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,6 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
const { state, toggleLeftPanel } = useApp();
const isNavCollapsed = state.layout.leftPanelCollapsed;
const [navWidth, setNavWidth] = useState(NAV_DEFAULT_WIDTH);
const [isDividerHovered, setIsDividerHovered] = useState(false);

const handleDividerMouseEnter = useCallback(() => {
setIsDividerHovered(true);
document.body.classList.add('bitfun-divider-hovered');
}, []);

const handleDividerMouseLeave = useCallback(() => {
setIsDividerHovered(false);
document.body.classList.remove('bitfun-divider-hovered');
}, []);

const handleNavCollapseDragStart = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (event.button !== 0 || isNavCollapsed) return;
Expand Down Expand Up @@ -93,7 +82,6 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
function cleanup() {
document.body.classList.remove('bitfun-is-dragging-nav-collapse');
document.body.classList.remove('bitfun-is-resizing-nav');
document.body.classList.remove('bitfun-divider-hovered');
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
Expand All @@ -103,7 +91,7 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
}, [isNavCollapsed, navWidth, toggleLeftPanel]);

return (
<div className={`bitfun-workspace-body${isEntering ? ' is-entering' : ''}${isExiting ? ' is-exiting' : ''}${isDividerHovered ? ' is-divider-hovered' : ''} ${className}`}>
<div className={`bitfun-workspace-body${isEntering ? ' is-entering' : ''}${isExiting ? ' is-exiting' : ''} ${className}`}>
{isNavCollapsed && (
<div className="bitfun-workspace-body__collapsed-nav">
<NavBar isCollapsed onExpandNav={toggleLeftPanel} onMaximize={onMaximize} />
Expand All @@ -125,8 +113,6 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
className="bitfun-workspace-body__nav-divider"
style={{ '--nav-width': `${navWidth}px` } as React.CSSProperties}
onMouseDown={handleNavCollapseDragStart}
onMouseEnter={handleDividerMouseEnter}
onMouseLeave={handleDividerMouseLeave}
role="separator"
aria-hidden="true"
/>
Expand Down
22 changes: 0 additions & 22 deletions src/web-ui/src/app/scenes/SceneViewport.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,6 @@
border-radius: inherit;
}

// Divider hover / resize: inner glow only (::after paints above scene content).
&__clip::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: $z-decoration;
border-radius: inherit;
opacity: 0;
transition: opacity 240ms cubic-bezier(0.25, 1, 0.5, 1);
box-shadow: inset 0 0 72px -18px var(--color-accent-200);
}

body.bitfun-divider-hovered &__clip::after,
body.bitfun-is-resizing-nav &__clip::after {
opacity: 1;
}

body.bitfun-is-resizing-nav &__clip::after {
box-shadow: inset 0 0 64px -16px color-mix(in srgb, var(--color-accent-500) 16%, transparent);
}

// ── Welcome overlay (app start) ──────────────────────

&__clip--welcome {
Expand Down
1 change: 1 addition & 0 deletions src/web-ui/src/app/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@use './global.scss';
@use './motion.scss';
@use '../layout/AppLayout.scss';
@use '../../infrastructure/mouse-glow/MouseGlowEffect.scss';

/* ========== Utility styles ========== */
@import './utilities/animations.css';
Expand Down
1 change: 1 addition & 0 deletions src/web-ui/src/component-library/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(({
return (
<div
ref={ref}
data-mouse-glow-surface=""
className={classNames}
{...props}
>
Expand Down
4 changes: 3 additions & 1 deletion src/web-ui/src/flow_chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3883,7 +3883,9 @@ export const ChatInput: React.FC<ChatInputProps> = ({

<div className="bitfun-chat-input__container">
<AcpPlanPanel entries={acpPlanEntries} />
<div className={`bitfun-chat-input__box ${isMultiLine ? 'bitfun-chat-input__box--multi-line' : 'bitfun-chat-input__box--capsule'}`}>
<div
className={`bitfun-chat-input__box ${isMultiLine ? 'bitfun-chat-input__box--multi-line' : 'bitfun-chat-input__box--capsule'}`}
>
{showTargetSwitcher && (
<div className="bitfun-chat-input__target-switcher" data-testid="chat-input-target-switcher">
<span className="bitfun-chat-input__target-switcher-label">{t('chatInput.conversationTarget')}</span>
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/flow_chat/components/FlowTextBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const RuntimeStatusBlock: React.FC<Pick<FlowTextBlockProps, 'textItem' | 'classN
data-testid={testId}
{...testAttributes}
>
<DotMatrixLoader size="medium" className="flow-text-block__runtime-status-icon" />
<DotMatrixLoader size="small" className="flow-text-block__runtime-status-icon" />
{hint && <span className="flow-text-block__runtime-status-text">{hint}</span>}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Processing indicator.
* After 1s of continuous processing, shows a 3×3 Rubik-style dot matrix and
* After 1s of continuous processing, shows a compact 3×3 Rubik-style dot matrix and
* rotating fun hint text together (matrix on the left).
* reserveSpace keeps layout height even when hidden.
*/
Expand Down Expand Up @@ -69,7 +69,7 @@ export const ProcessingIndicator: React.FC<ProcessingIndicatorProps> = ({ visibl
>
{showHint && hints.length > 0 && (
<>
<DotMatrixLoader size="medium" />
<DotMatrixLoader size="small" />
<span key={hintIndex} className="processing-indicator__hint">
{hints[hintIndex]}
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useCallback, useMemo } from 'react';
import { FontPreferencePanel } from '@/infrastructure/font-preference';
import { useMouseGlowPreference } from '@/infrastructure/mouse-glow';
import { useTranslation } from 'react-i18next';
import { Select, Tooltip } from '@/component-library';
import { Select, Switch, Tooltip } from '@/component-library';
import {
useTheme,
ThemeMetadata,
Expand Down Expand Up @@ -151,6 +152,32 @@ function AppearanceThemeSection() {
);
}

function AppearanceEffectsSection() {
const { t } = useTranslation('settings/appearance');
const { enabled, setEnabled } = useMouseGlowPreference();

return (
<ConfigPageSection
title={t('effects.title')}
description={t('effects.hint')}
>
<ConfigPageRow
label={t('effects.mouseGlow.label')}
description={t('effects.mouseGlow.description')}
align="center"
>
<Switch
checked={enabled}
onChange={(event) => setEnabled(event.target.checked)}
aria-label={t('effects.mouseGlow.label')}
data-testid="appearance-mouse-glow-switch"
size="small"
/>
</ConfigPageRow>
</ConfigPageSection>
);
}

interface ThemePreviewThumbnailProps {
theme: ThemeConfigType;
}
Expand Down Expand Up @@ -398,6 +425,7 @@ const AppearanceConfig: React.FC = () => {
<ConfigPageContent className="bitfun-appearance-config__content">
<ConfigPageSectionStack data-testid="appearance-config">
<AppearanceThemeSection />
<AppearanceEffectsSection />
<FontPreferencePanel />
</ConfigPageSectionStack>
</ConfigPageContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const ConfigPageSection: React.FC<ConfigPageSectionProps> = ({
</div>
)}
</div>
<div className="bitfun-config-page-section__body">
<div className="bitfun-config-page-section__body" data-mouse-glow-surface="">
{children}
</div>
</section>
Expand Down Expand Up @@ -175,4 +175,3 @@ export const ConfigPageRow: React.FC<ConfigPageRowProps> = ({

export default ConfigPageLayout;


88 changes: 88 additions & 0 deletions src/web-ui/src/infrastructure/mouse-glow/MouseGlowEffect.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
@use '../../component-library/styles/tokens' as tokens;

.bitfun-mouse-glow-overlay {
--mouse-glow-size: 220px;
--mouse-glow-border-width: 1px;
--mouse-glow-core: color-mix(
in srgb,
var(--color-accent-500) 78%,
var(--color-purple-500)
);
--mouse-glow-falloff: color-mix(
in srgb,
var(--color-accent-500) 22%,
transparent
);
--mouse-glow-shadow: color-mix(
in srgb,
var(--color-accent-500) 42%,
transparent
);

position: fixed;
z-index: tokens.$z-floating - 1;
top: 0;
left: 0;
box-sizing: border-box;
padding: var(--mouse-glow-border-width);
pointer-events: none;
opacity: 0;
visibility: hidden;
background-image: radial-gradient(
circle var(--mouse-glow-size) at var(--mouse-glow-local-x) var(--mouse-glow-local-y),
var(--mouse-glow-core) 0%,
var(--mouse-glow-falloff) 52%,
transparent 78%
);
background-repeat: no-repeat;
filter: drop-shadow(0 0 8px var(--mouse-glow-shadow));
-webkit-mask:
linear-gradient(currentColor 0 0) content-box,
linear-gradient(currentColor 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(currentColor 0 0) content-box,
linear-gradient(currentColor 0 0);
mask-composite: exclude;

&[data-active] {
opacity: 1;
visibility: visible;
}

&[data-divider] {
padding: 0;
-webkit-mask: none;
mask: none;
}

&[data-local-position] {
position: absolute;
}
}

:root[data-theme-type='light'] .bitfun-mouse-glow-overlay,
[data-theme-type='light'] .bitfun-mouse-glow-overlay {
--mouse-glow-border-width: 1.5px;
--mouse-glow-core: color-mix(
in srgb,
var(--color-static-black) 38%,
var(--color-static-white)
);
--mouse-glow-falloff: color-mix(
in srgb,
var(--color-static-black) 16%,
transparent
);
--mouse-glow-shadow: color-mix(
in srgb,
var(--color-static-black) 20%,
transparent
);
}

@media (prefers-reduced-motion: reduce) {
.bitfun-mouse-glow-overlay {
display: none;
}
}
Loading