diff --git a/packages/fossflow-lib/src/components/Label/Label.tsx b/packages/fossflow-lib/src/components/Label/Label.tsx index de2e589d..a4f18e60 100644 --- a/packages/fossflow-lib/src/components/Label/Label.tsx +++ b/packages/fossflow-lib/src/components/Label/Label.tsx @@ -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; @@ -11,6 +12,7 @@ export interface Props { children: React.ReactNode; sx?: SxProps; showLine?: boolean; + backgroundOpacity?: number; } export const Label = ({ @@ -20,9 +22,16 @@ export const Label = ({ expandDirection = 'CENTER', labelHeight = 0, sx, - showLine = true + showLine = true, + backgroundOpacity }: Props) => { const contentRef = useRef(); + const labelSettings = useUiStateStore((state) => { + return state.labelSettings; + }); + + // Use prop value if provided, otherwise fall back to store setting + const opacity = backgroundOpacity ?? labelSettings.backgroundOpacity; return ( { - 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({ @@ -17,17 +17,57 @@ export const LabelSettings = () => { }); }; + const handleOpacityChange = (_event: Event, value: number | number[]) => { + setLabelSettings({ + ...labelSettings, + backgroundOpacity: (value as number) / 100 + }); + }; + return ( Configure label display settings + + + Background Opacity + + + Adjust label background transparency to see nodes behind labels + + { + return `${value}%`; + }} + sx={{ mt: 2 }} + /> + + Current: {Math.round(labelSettings.backgroundOpacity * 100)}% + + + Expand Button Padding - + Bottom padding when expand button is visible (prevents text overlap) { + 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(); + + 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(); + + expect(screen.getByText('Expand Button Padding')).toBeInTheDocument(); + }); + + it('should display current opacity percentage', () => { + render(); + + expect(screen.getByText('Current: 100%')).toBeInTheDocument(); + }); + + it('should display current padding value', () => { + render(); + + expect(screen.getByText('Current: 0 theme units')).toBeInTheDocument(); + }); +}); diff --git a/packages/fossflow-lib/src/config/labelSettings.ts b/packages/fossflow-lib/src/config/labelSettings.ts index aacfe8b4..fe533abc 100644 --- a/packages/fossflow-lib/src/config/labelSettings.ts +++ b/packages/fossflow-lib/src/config/labelSettings.ts @@ -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 }; diff --git a/packages/fossflow-lib/src/interaction/useInteractionManager.ts b/packages/fossflow-lib/src/interaction/useInteractionManager.ts index c1a6496c..b19acefa 100644 --- a/packages/fossflow-lib/src/interaction/useInteractionManager.ts +++ b/packages/fossflow-lib/src/interaction/useInteractionManager.ts @@ -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'; @@ -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(() => { @@ -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 @@ -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'); @@ -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', @@ -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) => { @@ -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