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
13 changes: 11 additions & 2 deletions packages/fossflow-lib/src/components/Label/Label.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useRef } from 'react';
import { Box, SxProps } from '@mui/material';
import { useUiStateStore } from 'src/stores/uiStateStore';

const CONNECTOR_DOT_SIZE = 3;

Expand All @@ -11,6 +12,7 @@ export interface Props {
children: React.ReactNode;
sx?: SxProps;
showLine?: boolean;
backgroundOpacity?: number;
}

export const Label = ({
Expand All @@ -20,9 +22,16 @@ export const Label = ({
expandDirection = 'CENTER',
labelHeight = 0,
sx,
showLine = true
showLine = true,
backgroundOpacity
}: Props) => {
const contentRef = useRef<HTMLDivElement>();
const labelSettings = useUiStateStore((state) => {
return state.labelSettings;
});

// Use prop value if provided, otherwise fall back to store setting
const opacity = backgroundOpacity ?? labelSettings.backgroundOpacity;

return (
<Box
Expand Down Expand Up @@ -60,7 +69,7 @@ export const Label = ({
sx={{
position: 'absolute',
display: 'inline-block',
bgcolor: 'common.white',
bgcolor: `rgba(255, 255, 255, ${opacity})`,
border: '1px solid',
borderColor: 'grey.400',
borderRadius: 2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import {
Box,
Typography,
Slider
} from '@mui/material';
import { Box, Typography, Slider } from '@mui/material';
import { useUiStateStore } from 'src/stores/uiStateStore';

export const LabelSettings = () => {
const labelSettings = useUiStateStore((state) => state.labelSettings);
const setLabelSettings = useUiStateStore((state) => state.actions.setLabelSettings);
const labelSettings = useUiStateStore((state) => {
return state.labelSettings;
});
const setLabelSettings = useUiStateStore((state) => {
return state.actions.setLabelSettings;
});

const handlePaddingChange = (_event: Event, value: number | number[]) => {
setLabelSettings({
Expand All @@ -17,17 +17,57 @@ export const LabelSettings = () => {
});
};

const handleOpacityChange = (_event: Event, value: number | number[]) => {
setLabelSettings({
...labelSettings,
backgroundOpacity: (value as number) / 100
});
};

return (
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Configure label display settings
</Typography>

<Box sx={{ mb: 3 }}>
<Typography variant="body1" gutterBottom>
Background Opacity
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{ mb: 1, display: 'block' }}
>
Adjust label background transparency to see nodes behind labels
</Typography>
<Slider
value={Math.round(labelSettings.backgroundOpacity * 100)}
onChange={handleOpacityChange}
min={0}
max={100}
step={10}
marks
valueLabelDisplay="auto"
valueLabelFormat={(value) => {
return `${value}%`;
}}
sx={{ mt: 2 }}
/>
<Typography variant="caption" color="text.secondary">
Current: {Math.round(labelSettings.backgroundOpacity * 100)}%
</Typography>
</Box>

<Box sx={{ mb: 3 }}>
<Typography variant="body1" gutterBottom>
Expand Button Padding
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
<Typography
variant="caption"
color="text.secondary"
sx={{ mb: 1, display: 'block' }}
>
Bottom padding when expand button is visible (prevents text overlap)
</Typography>
<Slider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { LabelSettings } from '../LabelSettings';

const mockSetLabelSettings = jest.fn();
const mockLabelSettings = {
expandButtonPadding: 0,
backgroundOpacity: 1.0
};

jest.mock('../../../stores/uiStateStore', () => {
return {
useUiStateStore: jest.fn((selector) => {
const state = {
labelSettings: mockLabelSettings,
actions: {
setLabelSettings: mockSetLabelSettings
}
};
return selector ? selector(state) : state;
})
};
});

describe('LabelSettings', () => {
beforeEach(() => {
jest.clearAllMocks();
mockLabelSettings.backgroundOpacity = 1.0;
mockLabelSettings.expandButtonPadding = 0;
});

it('should render background opacity slider', () => {
render(<LabelSettings />);

expect(screen.getByText('Background Opacity')).toBeInTheDocument();
expect(
screen.getByText(
'Adjust label background transparency to see nodes behind labels'
)
).toBeInTheDocument();
});

it('should render expand button padding slider', () => {
render(<LabelSettings />);

expect(screen.getByText('Expand Button Padding')).toBeInTheDocument();
});

it('should display current opacity percentage', () => {
render(<LabelSettings />);

expect(screen.getByText('Current: 100%')).toBeInTheDocument();
});

it('should display current padding value', () => {
render(<LabelSettings />);

expect(screen.getByText('Current: 0 theme units')).toBeInTheDocument();
});
});
4 changes: 3 additions & 1 deletion packages/fossflow-lib/src/config/labelSettings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export interface LabelSettings {
expandButtonPadding: number; // Padding in theme units when expand button is visible
backgroundOpacity: number; // Background opacity (0-1), default 1.0
}

export const DEFAULT_LABEL_SETTINGS: LabelSettings = {
expandButtonPadding: 0 // Default 0 theme units (no extra padding)
expandButtonPadding: 0, // Default 0 theme units (no extra padding)
backgroundOpacity: 1.0 // Default fully opaque
};
65 changes: 56 additions & 9 deletions packages/fossflow-lib/src/interaction/useInteractionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { useModelStore } from 'src/stores/modelStore';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { ModeActions, State, SlimMouseEvent } from 'src/types';
import { DialogTypeEnum } from 'src/types/ui';
import { getMouse, getItemAtTile, generateId, incrementZoom, decrementZoom } from 'src/utils';
import {
getMouse,
getItemAtTile,
generateId,
incrementZoom,
decrementZoom
} from 'src/utils';
import { useResizeObserver } from 'src/hooks/useResizeObserver';
import { useScene } from 'src/hooks/useScene';
import { useHistory } from 'src/hooks/useHistory';
Expand Down Expand Up @@ -60,7 +66,10 @@ export const useInteractionManager = () => {
const { size: rendererSize } = useResizeObserver(uiState.rendererEl);
const { undo, redo, canUndo, canRedo } = useHistory();
const { createTextBox } = scene;
const { handleMouseDown: handlePanMouseDown, handleMouseUp: handlePanMouseUp } = usePanHandlers();
const {
handleMouseDown: handlePanMouseDown,
handleMouseUp: handlePanMouseUp
} = usePanHandlers();

// Keyboard shortcuts for undo/redo
useEffect(() => {
Expand All @@ -81,8 +90,10 @@ export const useInteractionManager = () => {

// Check if connection is in progress
const isConnectionInProgress =
(uiState.connectorInteractionMode === 'click' && connectorMode.isConnecting) ||
(uiState.connectorInteractionMode === 'drag' && connectorMode.id !== null);
(uiState.connectorInteractionMode === 'click' &&
connectorMode.isConnecting) ||
(uiState.connectorInteractionMode === 'drag' &&
connectorMode.id !== null);

if (isConnectionInProgress && connectorMode.id) {
// Delete the temporary connector
Expand Down Expand Up @@ -139,12 +150,29 @@ export const useInteractionManager = () => {
uiState.actions.setDialog(DialogTypeEnum.HELP);
}

// Toggle label transparency with ] key
if (e.key === ']') {
e.preventDefault();
const currentOpacity = uiState.labelSettings.backgroundOpacity;
// Toggle between full opacity (1.0) and semi-transparent (0.3)
const newOpacity = currentOpacity > 0.5 ? 0.3 : 1.0;
uiState.actions.setLabelSettings({
...uiState.labelSettings,
backgroundOpacity: newOpacity
});
}

// Tool hotkeys
const hotkeyMapping = HOTKEY_PROFILES[uiState.hotkeyProfile];
const key = e.key.toLowerCase();

// Quick icon selection for selected node (when ItemControls is an ItemReference with type 'ITEM')
if (key === 'i' && uiState.itemControls && 'id' in uiState.itemControls && uiState.itemControls.type === 'ITEM') {
if (
key === 'i' &&
uiState.itemControls &&
'id' in uiState.itemControls &&
uiState.itemControls.type === 'ITEM'
) {
e.preventDefault();
// Trigger icon change mode
const event = new CustomEvent('quickIconChange');
Expand Down Expand Up @@ -211,7 +239,10 @@ export const useInteractionManager = () => {
selection: null,
isDragging: false
});
} else if (hotkeyMapping.freehandLasso && key === hotkeyMapping.freehandLasso) {
} else if (
hotkeyMapping.freehandLasso &&
key === hotkeyMapping.freehandLasso
) {
e.preventDefault();
uiState.actions.setMode({
type: 'FREEHAND_LASSO',
Expand All @@ -227,7 +258,21 @@ export const useInteractionManager = () => {
return () => {
return window.removeEventListener('keydown', handleKeyDown);
};
}, [undo, redo, canUndo, canRedo, uiState.hotkeyProfile, uiState.actions, createTextBox, uiState.mouse.position.tile, scene, uiState.itemControls, uiState.mode, uiState.connectorInteractionMode]);
}, [
undo,
redo,
canUndo,
canRedo,
uiState.hotkeyProfile,
uiState.actions,
createTextBox,
uiState.mouse.position.tile,
scene,
uiState.itemControls,
uiState.mode,
uiState.connectorInteractionMode,
uiState.labelSettings
]);

const onMouseEvent = useCallback(
(e: SlimMouseEvent) => {
Expand Down Expand Up @@ -380,8 +425,10 @@ export const useInteractionManager = () => {

// The point under the cursor in world space (before zoom)
// World coordinates = (screen coordinates - scroll offset) / zoom
const worldX = (mouseRelativeToCenterX - uiState.scroll.position.x) / oldZoom;
const worldY = (mouseRelativeToCenterY - uiState.scroll.position.y) / oldZoom;
const worldX =
(mouseRelativeToCenterX - uiState.scroll.position.x) / oldZoom;
const worldY =
(mouseRelativeToCenterY - uiState.scroll.position.y) / oldZoom;

// After zooming, to keep the same world point under the cursor:
// screen coordinates = world coordinates * newZoom + scroll offset
Expand Down