diff --git a/packages/fossflow-lib/src/components/ColorSelector/CustomColorInput.tsx b/packages/fossflow-lib/src/components/ColorSelector/CustomColorInput.tsx
index 529e9f1a..bfdd4766 100644
--- a/packages/fossflow-lib/src/components/ColorSelector/CustomColorInput.tsx
+++ b/packages/fossflow-lib/src/components/ColorSelector/CustomColorInput.tsx
@@ -67,7 +67,7 @@ export const CustomColorInput = ({ value, onChange }: Props) => {
size="small"
InputProps={{
disableUnderline: true,
- sx: {
+ sx: {
fontSize: '0.875rem',
color: 'text.secondary',
width: '80px'
diff --git a/packages/fossflow-lib/src/components/ColorSelector/__tests__/ColorSelector.test.tsx b/packages/fossflow-lib/src/components/ColorSelector/__tests__/ColorSelector.test.tsx
index ff116b10..b3a6e40b 100644
--- a/packages/fossflow-lib/src/components/ColorSelector/__tests__/ColorSelector.test.tsx
+++ b/packages/fossflow-lib/src/components/ColorSelector/__tests__/ColorSelector.test.tsx
@@ -14,11 +14,15 @@ const mockColors = [
{ id: 'color5', value: '#FF00FF', name: 'Magenta' }
];
-jest.mock('../../../hooks/useScene', () => ({
- useScene: jest.fn(() => ({
- colors: mockColors
- }))
-}));
+jest.mock('../../../hooks/useScene', () => {
+ return {
+ useScene: jest.fn(() => {
+ return {
+ colors: mockColors
+ };
+ })
+ };
+});
describe('ColorSelector', () => {
const defaultProps = {
@@ -41,7 +45,7 @@ describe('ColorSelector', () => {
describe('rendering', () => {
it('should render all colors from the scene', () => {
renderComponent();
-
+
// Should render a button for each color
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(mockColors.length);
@@ -49,11 +53,11 @@ describe('ColorSelector', () => {
it('should render all color swatches', () => {
renderComponent();
-
+
// Should render a button for each color
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(mockColors.length);
-
+
// Each button should be clickable
buttons.forEach((button) => {
expect(button).toBeEnabled();
@@ -62,16 +66,20 @@ describe('ColorSelector', () => {
it('should render empty state when no colors available', () => {
const useScene = require('../../../hooks/useScene').useScene;
- useScene.mockImplementation(() => ({ colors: [] }));
-
+ useScene.mockImplementation(() => {
+ return { colors: [] };
+ });
+
const { container } = renderComponent();
-
+
// Should render container but no buttons
expect(container.firstChild).toBeInTheDocument();
expect(screen.queryAllByRole('button')).toHaveLength(0);
-
+
// Restore mock
- useScene.mockImplementation(() => ({ colors: mockColors }));
+ useScene.mockImplementation(() => {
+ return { colors: mockColors };
+ });
});
});
@@ -79,32 +87,32 @@ describe('ColorSelector', () => {
it('should call onChange with correct color ID when clicked', () => {
const onChange = jest.fn();
renderComponent({ onChange });
-
+
const buttons = screen.getAllByRole('button');
-
+
// Click the first color
fireEvent.click(buttons[0]);
expect(onChange).toHaveBeenCalledWith('color1');
-
+
// Click the third color
fireEvent.click(buttons[2]);
expect(onChange).toHaveBeenCalledWith('color3');
-
+
expect(onChange).toHaveBeenCalledTimes(2);
});
it('should handle multiple rapid clicks', () => {
const onChange = jest.fn();
renderComponent({ onChange });
-
+
const buttons = screen.getAllByRole('button');
-
+
// Rapidly click different colors
fireEvent.click(buttons[0]);
fireEvent.click(buttons[1]);
fireEvent.click(buttons[2]);
fireEvent.click(buttons[1]);
-
+
expect(onChange).toHaveBeenCalledTimes(4);
expect(onChange).toHaveBeenNthCalledWith(1, 'color1');
expect(onChange).toHaveBeenNthCalledWith(2, 'color2');
@@ -115,18 +123,18 @@ describe('ColorSelector', () => {
it('should be keyboard accessible', () => {
const onChange = jest.fn();
renderComponent({ onChange });
-
+
const buttons = screen.getAllByRole('button');
-
+
// All buttons should be focusable (have tabIndex)
buttons.forEach((button) => {
expect(button).toHaveAttribute('tabindex', '0');
});
-
+
// Buttons should respond to clicks (keyboard Enter/Space triggers click)
fireEvent.click(buttons[0]);
expect(onChange).toHaveBeenCalledWith('color1');
-
+
fireEvent.click(buttons[1]);
expect(onChange).toHaveBeenCalledWith('color2');
});
@@ -135,15 +143,15 @@ describe('ColorSelector', () => {
describe('active color indication', () => {
it('should indicate the active color with scaled transform', () => {
const { container } = renderComponent({ activeColor: 'color2' });
-
+
// Find all buttons (color swatches are inside buttons)
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(mockColors.length);
-
+
// The second button should contain the active color
// We can check if the active prop was passed correctly
const activeButton = buttons[1]; // color2 is at index 1
-
+
// Check that ColorSwatch received isActive=true for color2
// Since we can't easily check transform in JSDOM, we'll verify the component renders
expect(activeButton).toBeInTheDocument();
@@ -151,17 +159,17 @@ describe('ColorSelector', () => {
it('should update active indication when activeColor prop changes', () => {
const { rerender } = renderComponent({ activeColor: 'color1' });
-
+
let buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(mockColors.length);
-
+
// Change active color
rerender(
);
-
+
buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(mockColors.length);
// Verify buttons still render after prop change
@@ -170,7 +178,7 @@ describe('ColorSelector', () => {
it('should handle no active color', () => {
renderComponent({ activeColor: undefined });
-
+
// All buttons should still render
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(mockColors.length);
@@ -181,7 +189,7 @@ describe('ColorSelector', () => {
it('should handle invalid active color ID gracefully', () => {
renderComponent({ activeColor: 'invalid-color-id' });
-
+
// All buttons should still render even with invalid active color
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(mockColors.length);
@@ -194,50 +202,60 @@ describe('ColorSelector', () => {
describe('edge cases', () => {
it('should handle color with special characters in hex value', () => {
const useScene = require('../../../hooks/useScene').useScene;
- useScene.mockImplementation(() => ({
- colors: [
- { id: 'special', value: '#C0FFEE', name: 'Coffee' }
- ]
- }));
-
+ useScene.mockImplementation(() => {
+ return {
+ colors: [{ id: 'special', value: '#C0FFEE', name: 'Coffee' }]
+ };
+ });
+
const onChange = jest.fn();
renderComponent({ onChange });
-
+
const button = screen.getByRole('button');
fireEvent.click(button);
-
+
expect(onChange).toHaveBeenCalledWith('special');
-
+
// Restore mock
- useScene.mockImplementation(() => ({ colors: mockColors }));
+ useScene.mockImplementation(() => {
+ return { colors: mockColors };
+ });
});
it('should handle very long color lists efficiently', () => {
const useScene = require('../../../hooks/useScene').useScene;
- const manyColors = Array.from({ length: 100 }, (_, i) => ({
- id: `color${i}`,
- value: `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`,
- name: `Color ${i}`
- }));
-
- useScene.mockImplementation(() => ({ colors: manyColors }));
-
+ const manyColors = Array.from({ length: 100 }, (_, i) => {
+ return {
+ id: `color${i}`,
+ value: `#${Math.floor(Math.random() * 16777215)
+ .toString(16)
+ .padStart(6, '0')}`,
+ name: `Color ${i}`
+ };
+ });
+
+ useScene.mockImplementation(() => {
+ return { colors: manyColors };
+ });
+
const { container } = renderComponent();
-
+
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(100);
-
+
// Restore mock
- useScene.mockImplementation(() => ({ colors: mockColors }));
+ useScene.mockImplementation(() => {
+ return { colors: mockColors };
+ });
});
it('should handle onChange being required properly', () => {
// onChange is a required prop, so we test with a valid function
const onChange = jest.fn();
renderComponent({ onChange });
-
+
const buttons = screen.getAllByRole('button');
-
+
// Should work normally with onChange provided
fireEvent.click(buttons[0]);
expect(onChange).toHaveBeenCalledWith('color1');
@@ -246,32 +264,36 @@ describe('ColorSelector', () => {
it('should handle colors being updated dynamically', () => {
const useScene = require('../../../hooks/useScene').useScene;
const onChange = jest.fn();
-
+
// Start with 3 colors
- useScene.mockImplementation(() => ({
- colors: mockColors.slice(0, 3)
- }));
-
+ useScene.mockImplementation(() => {
+ return {
+ colors: mockColors.slice(0, 3)
+ };
+ });
+
const { rerender } = renderComponent({ onChange });
expect(screen.getAllByRole('button')).toHaveLength(3);
-
+
// Update to 5 colors
- useScene.mockImplementation(() => ({
- colors: mockColors
- }));
-
+ useScene.mockImplementation(() => {
+ return {
+ colors: mockColors
+ };
+ });
+
rerender(
);
-
+
expect(screen.getAllByRole('button')).toHaveLength(5);
-
+
// Click the newly added color
const buttons = screen.getAllByRole('button');
fireEvent.click(buttons[4]);
expect(onChange).toHaveBeenCalledWith('color5');
});
});
-});
\ No newline at end of file
+});
diff --git a/packages/fossflow-lib/src/components/ColorSelector/__tests__/CustomColorInput.test.tsx b/packages/fossflow-lib/src/components/ColorSelector/__tests__/CustomColorInput.test.tsx
index 86652719..14339d04 100644
--- a/packages/fossflow-lib/src/components/ColorSelector/__tests__/CustomColorInput.test.tsx
+++ b/packages/fossflow-lib/src/components/ColorSelector/__tests__/CustomColorInput.test.tsx
@@ -6,13 +6,28 @@ import { ThemeProvider } from '@mui/material/styles';
import { theme } from 'src/styles/theme';
// Mock ColorPicker since we don't need to test external library behavior
-jest.mock('../ColorPicker', () => ({
- ColorPicker: ({ value, onChange }: { value: string; onChange: (color: string) => void }) => (
-
onChange('#FFFFFF')}>
- {value}
-
- )
-}));
+jest.mock('../ColorPicker', () => {
+ return {
+ ColorPicker: ({
+ value,
+ onChange
+ }: {
+ value: string;
+ onChange: (color: string) => void;
+ }) => {
+ return (
+ {
+ return onChange('#FFFFFF');
+ }}
+ >
+ {value}
+
+ );
+ }
+ };
+});
describe('CustomColorInput', () => {
const defaultProps = {
@@ -42,7 +57,7 @@ describe('CustomColorInput', () => {
it('updates input value on change', () => {
renderComponent();
const input = screen.getByRole('textbox') as HTMLInputElement;
-
+
fireEvent.change(input, { target: { value: '#00FF00' } });
expect(input.value).toBe('#00FF00');
});
@@ -51,7 +66,7 @@ describe('CustomColorInput', () => {
const onChange = jest.fn();
renderComponent({ onChange });
const input = screen.getByRole('textbox');
-
+
fireEvent.change(input, { target: { value: '#00FF00' } });
expect(onChange).toHaveBeenCalledWith('#00FF00');
});
@@ -60,7 +75,7 @@ describe('CustomColorInput', () => {
const onChange = jest.fn();
renderComponent({ onChange });
const input = screen.getByRole('textbox');
-
+
fireEvent.change(input, { target: { value: 'invalid' } });
expect(onChange).not.toHaveBeenCalled();
});
@@ -68,10 +83,10 @@ describe('CustomColorInput', () => {
it('reverts to prop value on blur if input is invalid', () => {
renderComponent({ value: '#FF0000' });
const input = screen.getByRole('textbox') as HTMLInputElement;
-
+
fireEvent.change(input, { target: { value: 'invalid' } });
fireEvent.blur(input);
-
+
expect(input.value).toBe('#FF0000');
});
@@ -79,10 +94,10 @@ describe('CustomColorInput', () => {
const onChange = jest.fn();
renderComponent({ value: '#FF0000', onChange });
const input = screen.getByRole('textbox') as HTMLInputElement;
-
+
fireEvent.change(input, { target: { value: '#00FF00' } });
fireEvent.blur(input);
-
+
expect(input.value).toBe('#00FF00');
expect(onChange).toHaveBeenCalledWith('#00FF00');
});
@@ -105,9 +120,11 @@ describe('CustomColorInput', () => {
// Mock EyeDropper API
Object.defineProperty(window, 'EyeDropper', {
writable: true,
- value: jest.fn().mockImplementation(() => ({
- open: jest.fn().mockResolvedValue({ sRGBHex: '#123456' })
- }))
+ value: jest.fn().mockImplementation(() => {
+ return {
+ open: jest.fn().mockResolvedValue({ sRGBHex: '#123456' })
+ };
+ })
});
});
@@ -118,13 +135,15 @@ describe('CustomColorInput', () => {
it('renders eyedropper button when API is supported', () => {
renderComponent();
- expect(screen.getByRole('button', { name: /pick color/i })).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: /pick color/i })
+ ).toBeInTheDocument();
});
it('calls onChange with picked color', async () => {
const onChange = jest.fn();
renderComponent({ onChange });
-
+
const button = screen.getByRole('button', { name: /pick color/i });
await act(async () => {
fireEvent.click(button);
@@ -136,12 +155,14 @@ describe('CustomColorInput', () => {
it('handles EyeDropper cancellation gracefully', async () => {
const onChange = jest.fn();
// Mock rejection (user cancelled)
- (window.EyeDropper as any).mockImplementation(() => ({
- open: jest.fn().mockRejectedValue(new Error('Canceled'))
- }));
+ (window.EyeDropper as any).mockImplementation(() => {
+ return {
+ open: jest.fn().mockRejectedValue(new Error('Canceled'))
+ };
+ });
renderComponent({ onChange });
-
+
const button = screen.getByRole('button', { name: /pick color/i });
await act(async () => {
fireEvent.click(button);
@@ -159,7 +180,9 @@ describe('CustomColorInput', () => {
it('does not render eyedropper button when API is not supported', () => {
renderComponent();
- expect(screen.queryByRole('button', { name: /pick color/i })).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: /pick color/i })
+ ).not.toBeInTheDocument();
});
});
});
diff --git a/packages/fossflow-lib/src/components/ConnectorEmptySpaceTooltip/ConnectorEmptySpaceTooltip.tsx b/packages/fossflow-lib/src/components/ConnectorEmptySpaceTooltip/ConnectorEmptySpaceTooltip.tsx
index 55f0a44e..9e621010 100644
--- a/packages/fossflow-lib/src/components/ConnectorEmptySpaceTooltip/ConnectorEmptySpaceTooltip.tsx
+++ b/packages/fossflow-lib/src/components/ConnectorEmptySpaceTooltip/ConnectorEmptySpaceTooltip.tsx
@@ -6,15 +6,19 @@ import { useScene } from 'src/hooks/useScene';
export const ConnectorEmptySpaceTooltip = () => {
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
- const mode = useUiStateStore((state) => state.mode);
- const mouse = useUiStateStore((state) => state.mouse);
+ const mode = useUiStateStore((state) => {
+ return state.mode;
+ });
+ const mouse = useUiStateStore((state) => {
+ return state.mouse;
+ });
const { connectors } = useScene();
const previousModeRef = useRef(mode);
const shownForConnectorRef = useRef(null);
useEffect(() => {
const previousMode = previousModeRef.current;
-
+
// Detect when we transition from isConnecting to not isConnecting (connection completed)
if (
mode.type === 'CONNECTOR' &&
@@ -25,13 +29,18 @@ export const ConnectorEmptySpaceTooltip = () => {
) {
// Find the most recently created connector
const latestConnector = connectors[connectors.length - 1];
-
- if (latestConnector && latestConnector.id !== shownForConnectorRef.current) {
+
+ if (
+ latestConnector &&
+ latestConnector.id !== shownForConnectorRef.current
+ ) {
// Check if either end is connected to empty space (tile reference)
const hasEmptySpaceConnection = latestConnector.anchors.some(
- anchor => anchor.ref.tile && !anchor.ref.item
+ (anchor) => {
+ return anchor.ref.tile && !anchor.ref.item;
+ }
);
-
+
if (hasEmptySpaceConnection) {
// Show tooltip near the mouse position
setTooltipPosition({
@@ -40,22 +49,24 @@ export const ConnectorEmptySpaceTooltip = () => {
});
setShowTooltip(true);
shownForConnectorRef.current = latestConnector.id;
-
+
// Auto-hide after 12 seconds
const timer = setTimeout(() => {
setShowTooltip(false);
}, 12000);
-
- return () => clearTimeout(timer);
+
+ return () => {
+ return clearTimeout(timer);
+ };
}
}
}
-
+
// Hide tooltip when switching away from connector mode
if (mode.type !== 'CONNECTOR') {
setShowTooltip(false);
}
-
+
previousModeRef.current = mode;
}, [mode, connectors, mouse.position.screen]);
@@ -88,10 +99,12 @@ export const ConnectorEmptySpaceTooltip = () => {
}}
>
- To connect this connector to a node, left-click on the end of the connector and drag it to the desired node.
+ To connect this connector to a node,{' '}
+ left-click on the end of the connector and drag it
+ to the desired node.
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/ConnectorHintTooltip/ConnectorHintTooltip.tsx b/packages/fossflow-lib/src/components/ConnectorHintTooltip/ConnectorHintTooltip.tsx
index 51be4206..3f587061 100644
--- a/packages/fossflow-lib/src/components/ConnectorHintTooltip/ConnectorHintTooltip.tsx
+++ b/packages/fossflow-lib/src/components/ConnectorHintTooltip/ConnectorHintTooltip.tsx
@@ -13,8 +13,12 @@ interface Props {
export const ConnectorHintTooltip = ({ toolMenuRef }: Props) => {
const { t } = useTranslation('connectorHintTooltip');
const theme = useTheme();
- const connectorInteractionMode = useUiStateStore((state) => state.connectorInteractionMode);
- const mode = useUiStateStore((state) => state.mode);
+ const connectorInteractionMode = useUiStateStore((state) => {
+ return state.connectorInteractionMode;
+ });
+ const mode = useUiStateStore((state) => {
+ return state.mode;
+ });
const [isDismissed, setIsDismissed] = useState(true);
const [position, setPosition] = useState({ top: 16, right: 16 });
@@ -85,17 +89,25 @@ export const ConnectorHintTooltip = ({ toolMenuRef }: Props) => {
>
-
+
- {connectorInteractionMode === 'click' ? t('tipCreatingConnectors') : t('tipConnectorTools')}
+ {connectorInteractionMode === 'click'
+ ? t('tipCreatingConnectors')
+ : t('tipConnectorTools')}
-
+
{connectorInteractionMode === 'click' ? (
<>
- {t('clickInstructionStart')} {t('clickInstructionMiddle')} {t('clickInstructionStart')} {t('clickInstructionEnd')}
+ {t('clickInstructionStart')}{' '}
+ {t('clickInstructionMiddle')}{' '}
+ {t('clickInstructionStart')}{' '}
+ {t('clickInstructionEnd')}
{mode.type === 'CONNECTOR' && mode.isConnecting && (
-
+
{t('nowClickTarget')}
)}
@@ -106,11 +118,12 @@ export const ConnectorHintTooltip = ({ toolMenuRef }: Props) => {
>
)}
-
+
- {t('rerouteStart')} {t('rerouteMiddle')} {t('rerouteEnd')}
+ {t('rerouteStart')} {t('rerouteMiddle')}{' '}
+ {t('rerouteEnd')}
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/ConnectorRerouteTooltip/ConnectorRerouteTooltip.tsx b/packages/fossflow-lib/src/components/ConnectorRerouteTooltip/ConnectorRerouteTooltip.tsx
index bda5fe81..25df1a51 100644
--- a/packages/fossflow-lib/src/components/ConnectorRerouteTooltip/ConnectorRerouteTooltip.tsx
+++ b/packages/fossflow-lib/src/components/ConnectorRerouteTooltip/ConnectorRerouteTooltip.tsx
@@ -11,8 +11,12 @@ export const ConnectorRerouteTooltip = () => {
const { t } = useTranslation('connectorRerouteTooltip');
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
- const mode = useUiStateStore((state) => state.mode);
- const mouse = useUiStateStore((state) => state.mouse);
+ const mode = useUiStateStore((state) => {
+ return state.mode;
+ });
+ const mouse = useUiStateStore((state) => {
+ return state.mouse;
+ });
const { connectors } = useScene();
const previousModeRef = useRef(mode);
const shownForConnectorRef = useRef(null);
@@ -44,7 +48,10 @@ export const ConnectorRerouteTooltip = () => {
// Find the most recently created connector
const latestConnector = connectors[connectors.length - 1];
- if (latestConnector && latestConnector.id !== shownForConnectorRef.current) {
+ if (
+ latestConnector &&
+ latestConnector.id !== shownForConnectorRef.current
+ ) {
// Show tooltip near the mouse position
setTooltipPosition({
x: mouse.position.screen.x,
@@ -58,7 +65,9 @@ export const ConnectorRerouteTooltip = () => {
setShowTooltip(false);
}, 15000);
- return () => clearTimeout(timer);
+ return () => {
+ return clearTimeout(timer);
+ };
}
}
@@ -122,7 +131,9 @@ export const ConnectorRerouteTooltip = () => {
- {t('instructionSelect')} {t('instructionMiddle')} {t('instructionClick')} {t('instructionAnd')} {t('instructionDrag')} {t('instructionEnd')}
+ {t('instructionSelect')} {t('instructionMiddle')}{' '}
+ {t('instructionClick')} {t('instructionAnd')}{' '}
+ {t('instructionDrag')} {t('instructionEnd')}
diff --git a/packages/fossflow-lib/src/components/ConnectorSettings/ConnectorSettings.tsx b/packages/fossflow-lib/src/components/ConnectorSettings/ConnectorSettings.tsx
index 29c0d646..63538003 100644
--- a/packages/fossflow-lib/src/components/ConnectorSettings/ConnectorSettings.tsx
+++ b/packages/fossflow-lib/src/components/ConnectorSettings/ConnectorSettings.tsx
@@ -13,8 +13,12 @@ import { useUiStateStore } from 'src/stores/uiStateStore';
import { useTranslation } from 'src/stores/localeStore';
export const ConnectorSettings = () => {
- const connectorInteractionMode = useUiStateStore((state) => state.connectorInteractionMode);
- const setConnectorInteractionMode = useUiStateStore((state) => state.actions.setConnectorInteractionMode);
+ const connectorInteractionMode = useUiStateStore((state) => {
+ return state.connectorInteractionMode;
+ });
+ const setConnectorInteractionMode = useUiStateStore((state) => {
+ return state.actions.setConnectorInteractionMode;
+ });
const { t } = useTranslation();
const handleChange = (event: React.ChangeEvent) => {
@@ -29,7 +33,9 @@ export const ConnectorSettings = () => {
- {t('settings.connector.connectionMode')}
+
+ {t('settings.connector.connectionMode')}
+
{
control={}
label={
- {t('settings.connector.clickMode')}
+
+ {t('settings.connector.clickMode')}
+
{t('settings.connector.clickModeDesc')}
@@ -52,7 +60,9 @@ export const ConnectorSettings = () => {
control={}
label={
- {t('settings.connector.dragMode')}
+
+ {t('settings.connector.dragMode')}
+
{t('settings.connector.dragModeDesc')}
@@ -69,4 +79,4 @@ export const ConnectorSettings = () => {
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/ContextMenu/ContextMenu.tsx b/packages/fossflow-lib/src/components/ContextMenu/ContextMenu.tsx
index 5f3c1e13..dffbd3a3 100644
--- a/packages/fossflow-lib/src/components/ContextMenu/ContextMenu.tsx
+++ b/packages/fossflow-lib/src/components/ContextMenu/ContextMenu.tsx
@@ -31,7 +31,11 @@ export const ContextMenu = ({
onClose={onClose}
>
{menuItems.map((item, index) => {
- return ;
+ return (
+
+ );
})}
);
diff --git a/packages/fossflow-lib/src/components/ContextMenu/ContextMenuManager.tsx b/packages/fossflow-lib/src/components/ContextMenu/ContextMenuManager.tsx
index f7526b50..da8e6c8e 100644
--- a/packages/fossflow-lib/src/components/ContextMenu/ContextMenuManager.tsx
+++ b/packages/fossflow-lib/src/components/ContextMenu/ContextMenuManager.tsx
@@ -1,6 +1,11 @@
import React, { useCallback } from 'react';
import { useUiStateStore } from 'src/stores/uiStateStore';
-import { getTilePosition, CoordsUtils, generateId, findNearestUnoccupiedTile } from 'src/utils';
+import {
+ getTilePosition,
+ CoordsUtils,
+ generateId,
+ findNearestUnoccupiedTile
+} from 'src/utils';
import { useScene } from 'src/hooks/useScene';
import { useModelStore } from 'src/stores/modelStore';
import { VIEW_ITEM_DEFAULTS } from 'src/config';
@@ -50,9 +55,11 @@ export const ContextMenuManager = ({ anchorEl }: Props) => {
if (model.icons.length > 0) {
const modelItemId = generateId();
const firstIcon = model.icons[0];
-
+
// Find nearest unoccupied tile (should return the same tile since context menu is for empty tiles)
- const targetTile = findNearestUnoccupiedTile(contextMenu.tile, scene) || contextMenu.tile;
+ const targetTile =
+ findNearestUnoccupiedTile(contextMenu.tile, scene) ||
+ contextMenu.tile;
scene.placeIcon({
modelItem: {
diff --git a/packages/fossflow-lib/src/components/ExportImageDialog/ExportImageDialog.tsx b/packages/fossflow-lib/src/components/ExportImageDialog/ExportImageDialog.tsx
index 4329caca..0460b8ae 100644
--- a/packages/fossflow-lib/src/components/ExportImageDialog/ExportImageDialog.tsx
+++ b/packages/fossflow-lib/src/components/ExportImageDialog/ExportImageDialog.tsx
@@ -55,12 +55,16 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
const isExporting = useRef(false);
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState(null);
- const currentView = useUiStateStore((state) => state.view);
+ const currentView = useUiStateStore((state) => {
+ return state.view;
+ });
const [imageData, setImageData] = React.useState();
const [croppedImageData, setCroppedImageData] = useState();
const [exportError, setExportError] = useState(false);
const { getUnprojectedBounds } = useDiagramUtils();
- const uiStateActions = useUiStateStore((state) => state.actions);
+ const uiStateActions = useUiStateStore((state) => {
+ return state.actions;
+ });
const model = useModelStore((state): Omit => {
return modelFromModelStore(state);
});
@@ -113,7 +117,12 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
height: bounds.height
};
- exportAsImage(containerRef.current as HTMLDivElement, containerSize, exportScale, transparentBackground ? 'transparent' : backgroundColor)
+ exportAsImage(
+ containerRef.current as HTMLDivElement,
+ containerSize,
+ exportScale,
+ transparentBackground ? 'transparent' : backgroundColor
+ )
.then((data) => {
setImageData(data);
isExporting.current = false;
@@ -142,7 +151,7 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
const scaleX = img.width / displayCanvas.width;
const scaleY = img.height / displayCanvas.height;
-
+
// Calculate the actual crop area in the source image coordinates
const actualCropArea = {
x: cropArea.x * scaleX,
@@ -159,17 +168,25 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
// Draw the cropped portion from the source image
ctx.drawImage(
img,
- actualCropArea.x, actualCropArea.y, actualCropArea.width, actualCropArea.height,
- 0, 0, actualCropArea.width, actualCropArea.height
+ actualCropArea.x,
+ actualCropArea.y,
+ actualCropArea.width,
+ actualCropArea.height,
+ 0,
+ 0,
+ actualCropArea.width,
+ actualCropArea.height
);
-
+
resolve(canvas.toDataURL('image/png'));
} else {
reject(new Error('Could not get canvas context'));
}
};
- img.onerror = () => reject(new Error('Failed to load image'));
+ img.onerror = () => {
+ return reject(new Error('Failed to load image'));
+ };
img.src = sourceImage;
});
}, []);
@@ -186,50 +203,59 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
}, [cropArea, imageData, cropToContent, cropImage, isInCropMode]);
// Mouse handlers for crop selection
- const handleMouseDown = useCallback((e: React.MouseEvent) => {
- if (!isInCropMode) return;
-
- e.preventDefault();
- const canvas = cropCanvasRef.current;
- if (!canvas) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- setDragStart({ x, y });
- setIsDragging(true);
- setCropArea(null);
- }, [isInCropMode]);
+ const handleMouseDown = useCallback(
+ (e: React.MouseEvent) => {
+ if (!isInCropMode) return;
- const handleMouseMove = useCallback((e: React.MouseEvent) => {
- if (!isDragging || !dragStart || !isInCropMode) return;
-
- e.preventDefault();
- const canvas = cropCanvasRef.current;
- if (!canvas) return;
+ e.preventDefault();
+ const canvas = cropCanvasRef.current;
+ if (!canvas) return;
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
- const newCropArea: CropArea = {
- x: Math.min(dragStart.x, x),
- y: Math.min(dragStart.y, y),
- width: Math.abs(x - dragStart.x),
- height: Math.abs(y - dragStart.y)
- };
+ setDragStart({ x, y });
+ setIsDragging(true);
+ setCropArea(null);
+ },
+ [isInCropMode]
+ );
- setCropArea(newCropArea);
- }, [isDragging, dragStart, isInCropMode]);
+ const handleMouseMove = useCallback(
+ (e: React.MouseEvent) => {
+ if (!isDragging || !dragStart || !isInCropMode) return;
- const handleMouseUp = useCallback((e: React.MouseEvent) => {
- if (!isDragging) return;
-
- e.preventDefault();
- setIsDragging(false);
- setDragStart(null);
- }, [isDragging]);
+ e.preventDefault();
+ const canvas = cropCanvasRef.current;
+ if (!canvas) return;
+
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ const newCropArea: CropArea = {
+ x: Math.min(dragStart.x, x),
+ y: Math.min(dragStart.y, y),
+ width: Math.abs(x - dragStart.x),
+ height: Math.abs(y - dragStart.y)
+ };
+
+ setCropArea(newCropArea);
+ },
+ [isDragging, dragStart, isInCropMode]
+ );
+
+ const handleMouseUp = useCallback(
+ (e: React.MouseEvent) => {
+ if (!isDragging) return;
+
+ e.preventDefault();
+ setIsDragging(false);
+ setDragStart(null);
+ },
+ [isDragging]
+ );
// Add mouse leave handler to stop dragging when leaving canvas
const handleMouseLeave = useCallback(() => {
@@ -250,50 +276,63 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
// Calculate scaling factors between canvas and actual image
const scaleX = img.width / canvas.width;
const scaleY = img.height / canvas.height;
-
+
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
-
+
// Draw checkerboard if transparent background
if (transparentBackground) {
const squareSize = 10;
for (let y = 0; y < canvas.height; y += squareSize) {
for (let x = 0; x < canvas.width; x += squareSize) {
- ctx.fillStyle = (x / squareSize + y / squareSize) % 2 === 0 ? '#f0f0f0' : 'transparent';
+ ctx.fillStyle =
+ (x / squareSize + y / squareSize) % 2 === 0
+ ? '#f0f0f0'
+ : 'transparent';
ctx.fillRect(x, y, squareSize, squareSize);
}
}
}
-
+
// Draw the image scaled to fit canvas
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
-
+
// Draw crop overlay if in crop mode
if (isInCropMode) {
// Semi-transparent overlay
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
-
+
// Clear crop area and draw border only if there's a valid selection
if (cropArea && cropArea.width > 5 && cropArea.height > 5) {
// Clear the selected area (remove overlay)
- ctx.clearRect(cropArea.x, cropArea.y, cropArea.width, cropArea.height);
-
+ ctx.clearRect(
+ cropArea.x,
+ cropArea.y,
+ cropArea.width,
+ cropArea.height
+ );
+
// Redraw the original image in the selected area
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
-
+
// Redraw the overlay everywhere except the selected area
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
-
+
// Top area
if (cropArea.y > 0) {
ctx.fillRect(0, 0, canvas.width, cropArea.y);
}
// Bottom area
if (cropArea.y + cropArea.height < canvas.height) {
- ctx.fillRect(0, cropArea.y + cropArea.height, canvas.width, canvas.height - (cropArea.y + cropArea.height));
+ ctx.fillRect(
+ 0,
+ cropArea.y + cropArea.height,
+ canvas.width,
+ canvas.height - (cropArea.y + cropArea.height)
+ );
}
// Left area
if (cropArea.x > 0) {
@@ -301,17 +340,27 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
}
// Right area
if (cropArea.x + cropArea.width < canvas.width) {
- ctx.fillRect(cropArea.x + cropArea.width, cropArea.y, canvas.width - (cropArea.x + cropArea.width), cropArea.height);
+ ctx.fillRect(
+ cropArea.x + cropArea.width,
+ cropArea.y,
+ canvas.width - (cropArea.x + cropArea.width),
+ cropArea.height
+ );
}
-
+
ctx.restore();
-
+
// Draw crop border
ctx.strokeStyle = '#2196f3';
ctx.lineWidth = 2;
- ctx.strokeRect(cropArea.x, cropArea.y, cropArea.width, cropArea.height);
+ ctx.strokeRect(
+ cropArea.x,
+ cropArea.y,
+ cropArea.width,
+ cropArea.height
+ );
}
-
+
// Add instruction text only when no selection or dragging
if (!cropArea || cropArea.width <= 5 || cropArea.height <= 5) {
ctx.fillStyle = 'white';
@@ -321,7 +370,7 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
}
}
};
-
+
img.src = imageData;
}, [imageData, isInCropMode, cropArea, transparentBackground]);
@@ -386,16 +435,28 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
const timer = setTimeout(() => {
exportImage();
}, 200);
- return () => clearTimeout(timer);
+ return () => {
+ return clearTimeout(timer);
+ };
}
- }, [showGrid, backgroundColor, expandLabels, exportImage, cropToContent, exportScale, transparentBackground]);
+ }, [
+ showGrid,
+ backgroundColor,
+ expandLabels,
+ exportImage,
+ cropToContent,
+ exportScale,
+ transparentBackground
+ ]);
useEffect(() => {
if (!imageData) {
const timer = setTimeout(() => {
exportImage();
}, 200);
- return () => clearTimeout(timer);
+ return () => {
+ return clearTimeout(timer);
+ };
}
}, [exportImage, imageData]);
@@ -419,11 +480,9 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
-
- Browser Compatibility Notice
-
+ Browser Compatibility Notice
- For best results, please use Chrome or Edge. Firefox currently has
+ For best results, please use Chrome or Edge. Firefox currently has
compatibility issues with the export feature.
@@ -490,7 +549,11 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
style={{
maxWidth: '100%',
maxHeight: '300px',
- cursor: isInCropMode ? (isDragging ? 'grabbing' : 'crosshair') : 'default',
+ cursor: isInCropMode
+ ? isDragging
+ ? 'grabbing'
+ : 'crosshair'
+ : 'default',
border: isInCropMode ? '2px solid #2196f3' : 'none',
userSelect: 'none'
}}
@@ -498,7 +561,9 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
- onContextMenu={(e) => e.preventDefault()}
+ onContextMenu={(e) => {
+ return e.preventDefault();
+ }}
/>
{isInCropMode && (
@@ -515,9 +580,15 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
maxWidth: '100%',
maxHeight: '300px',
objectFit: 'contain',
- backgroundImage: transparentBackground ? 'linear-gradient(45deg, #f0f0f0 25%, transparent 25%), linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f0f0f0 75%), linear-gradient(-45deg, transparent 75%, #f0f0f0 75%)' : undefined,
- backgroundSize: transparentBackground ? '20px 20px' : undefined,
- backgroundPosition: transparentBackground ? '0 0, 0 10px, 10px -10px, -10px 0px' : undefined
+ backgroundImage: transparentBackground
+ ? 'linear-gradient(45deg, #f0f0f0 25%, transparent 25%), linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f0f0f0 75%), linear-gradient(-45deg, transparent 75%, #f0f0f0 75%)'
+ : undefined,
+ backgroundSize: transparentBackground
+ ? '20px 20px'
+ : undefined,
+ backgroundPosition: transparentBackground
+ ? '0 0, 0 10px, 10px -10px, -10px 0px'
+ : undefined
}}
src={displayImage}
alt="preview"
@@ -609,11 +680,13 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
}
}}
>
- {dpiPresets.map((preset) => (
-
- ))}
+ {dpiPresets.map((preset) => {
+ return (
+
+ );
+ })}
@@ -621,11 +694,14 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
{scaleMode === 'custom' && (
- Scale: {exportScale.toFixed(1)}x ({(exportScale * 72).toFixed(0)} DPI)
+ Scale: {exportScale.toFixed(1)}x (
+ {(exportScale * 72).toFixed(0)} DPI)
setExportScale(value as number)}
+ onChange={(_, value) => {
+ return setExportScale(value as number);
+ }}
min={1}
max={5}
step={0.1}
@@ -637,7 +713,9 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
{ value: 5, label: '5x' }
]}
valueLabelDisplay="auto"
- valueLabelFormat={(value) => `${value.toFixed(1)}x`}
+ valueLabelFormat={(value) => {
+ return `${value.toFixed(1)}x`;
+ }}
/>
)}
@@ -649,25 +727,43 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
{croppedImageData ? (
-
) : cropArea ? (
-
+
Apply Crop
- setCropArea(null)}>
+ {
+ return setCropArea(null);
+ }}
+ >
Clear Selection
) : isInCropMode ? (
- Select an area to crop, or uncheck "Crop to content" to use full image
+ Select an area to crop, or uncheck "Crop to content" to
+ use full image
) : null}
@@ -680,9 +776,11 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
Cancel
-
Download as PNG
@@ -698,4 +796,4 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/HelpDialog/HelpDialog.tsx b/packages/fossflow-lib/src/components/HelpDialog/HelpDialog.tsx
index 18e9dd33..9f4c1176 100644
--- a/packages/fossflow-lib/src/components/HelpDialog/HelpDialog.tsx
+++ b/packages/fossflow-lib/src/components/HelpDialog/HelpDialog.tsx
@@ -29,7 +29,7 @@ interface ShortcutItem {
export const HelpDialog = () => {
const { t } = useTranslation('helpDialog');
-
+
const dialog = useUiStateStore((state) => {
return state.dialog;
});
@@ -162,9 +162,15 @@ export const HelpDialog = () => {
- {t('action')}
- {t('shortcut')}
- {t('description')}
+
+ {t('action')}
+
+
+ {t('shortcut')}
+
+
+ {t('description')}
+
@@ -203,9 +209,15 @@ export const HelpDialog = () => {
- {t('action')}
- {t('method')}
- {t('description')}
+
+ {t('action')}
+
+
+ {t('method')}
+
+
+ {t('description')}
+
diff --git a/packages/fossflow-lib/src/components/HotkeySettings/HotkeySettings.tsx b/packages/fossflow-lib/src/components/HotkeySettings/HotkeySettings.tsx
index 7e23bd19..fbb93439 100644
--- a/packages/fossflow-lib/src/components/HotkeySettings/HotkeySettings.tsx
+++ b/packages/fossflow-lib/src/components/HotkeySettings/HotkeySettings.tsx
@@ -19,8 +19,12 @@ import { HOTKEY_PROFILES, HotkeyProfile } from 'src/config/hotkeys';
import { useTranslation } from 'src/stores/localeStore';
export const HotkeySettings = () => {
- const hotkeyProfile = useUiStateStore((state) => state.hotkeyProfile);
- const setHotkeyProfile = useUiStateStore((state) => state.actions.setHotkeyProfile);
+ const hotkeyProfile = useUiStateStore((state) => {
+ return state.hotkeyProfile;
+ });
+ const setHotkeyProfile = useUiStateStore((state) => {
+ return state.actions.setHotkeyProfile;
+ });
const { t } = useTranslation();
const currentMapping = HOTKEY_PROFILES[hotkeyProfile];
@@ -29,8 +33,14 @@ export const HotkeySettings = () => {
{ name: t('settings.hotkeys.toolSelect'), key: currentMapping.select },
{ name: t('settings.hotkeys.toolPan'), key: currentMapping.pan },
{ name: t('settings.hotkeys.toolAddItem'), key: currentMapping.addItem },
- { name: t('settings.hotkeys.toolRectangle'), key: currentMapping.rectangle },
- { name: t('settings.hotkeys.toolConnector'), key: currentMapping.connector },
+ {
+ name: t('settings.hotkeys.toolRectangle'),
+ key: currentMapping.rectangle
+ },
+ {
+ name: t('settings.hotkeys.toolConnector'),
+ key: currentMapping.connector
+ },
{ name: t('settings.hotkeys.toolText'), key: currentMapping.text }
];
@@ -45,10 +55,16 @@ export const HotkeySettings = () => {
@@ -63,24 +79,33 @@ export const HotkeySettings = () => {
- {tools.map((tool) => (
-
- {tool.name}
-
-
- {tool.key ? tool.key.toUpperCase() : '-'}
-
-
-
- ))}
+ {tools.map((tool) => {
+ return (
+
+ {tool.name}
+
+
+ {tool.key ? tool.key.toUpperCase() : '-'}
+
+
+
+ );
+ })}
)}
-
+
{t('settings.hotkeys.note')}
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/IconPackSettings/IconPackSettings.tsx b/packages/fossflow-lib/src/components/IconPackSettings/IconPackSettings.tsx
index 07eb4a1f..37280feb 100644
--- a/packages/fossflow-lib/src/components/IconPackSettings/IconPackSettings.tsx
+++ b/packages/fossflow-lib/src/components/IconPackSettings/IconPackSettings.tsx
@@ -38,12 +38,16 @@ export const IconPackSettings: React.FC = ({
}) => {
const { t } = useTranslation();
- const handleLazyLoadingChange = (event: React.ChangeEvent) => {
+ const handleLazyLoadingChange = (
+ event: React.ChangeEvent
+ ) => {
onToggleLazyLoading(event.target.checked);
};
- const handlePackToggle = (packName: string) => (event: React.ChangeEvent) => {
- onTogglePack(packName, event.target.checked);
+ const handlePackToggle = (packName: string) => {
+ return (event: React.ChangeEvent) => {
+ onTogglePack(packName, event.target.checked);
+ };
};
return (
@@ -55,7 +59,13 @@ export const IconPackSettings: React.FC = ({
{/* Lazy Loading Toggle */}
-
+
{t('settings.iconPacks.lazyLoading')}
@@ -75,7 +85,13 @@ export const IconPackSettings: React.FC = ({
{/* Core Isoflow (Always Loaded) */}
-
+
{t('settings.iconPacks.coreIsoflow')}
@@ -101,47 +117,68 @@ export const IconPackSettings: React.FC = ({
)}
- {packInfo.map((pack) => (
-
-
-
-
- {pack.displayName}
-
-
- {pack.loading && (
- <>
-
+ {packInfo.map((pack) => {
+ return (
+
+
+
+
+ {pack.displayName}
+
+
+ {pack.loading && (
+ <>
+
+
+ {t('settings.iconPacks.loading')}
+
+ >
+ )}
+ {pack.loaded && !pack.loading && (
+
+ {t('settings.iconPacks.loaded')} •{' '}
+ {t('settings.iconPacks.iconCount').replace(
+ '{count}',
+ String(pack.iconCount)
+ )}
+
+ )}
+ {pack.error && (
+
+ {pack.error}
+
+ )}
+ {!pack.loaded && !pack.loading && !pack.error && (
- {t('settings.iconPacks.loading')}
+ {t('settings.iconPacks.notLoaded')}
- >
- )}
- {pack.loaded && !pack.loading && (
-
- {t('settings.iconPacks.loaded')} • {t('settings.iconPacks.iconCount').replace('{count}', String(pack.iconCount))}
-
- )}
- {pack.error && (
-
- {pack.error}
-
- )}
- {!pack.loaded && !pack.loading && !pack.error && (
-
- {t('settings.iconPacks.notLoaded')}
-
- )}
+ )}
+
+
-
-
-
- ))}
+
+ );
+ })}
diff --git a/packages/fossflow-lib/src/components/ImportHintTooltip/ImportHintTooltip.tsx b/packages/fossflow-lib/src/components/ImportHintTooltip/ImportHintTooltip.tsx
index 97aeadd6..2e94d727 100644
--- a/packages/fossflow-lib/src/components/ImportHintTooltip/ImportHintTooltip.tsx
+++ b/packages/fossflow-lib/src/components/ImportHintTooltip/ImportHintTooltip.tsx
@@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Box, IconButton, Paper, Typography } from '@mui/material';
-import { Close as CloseIcon, FolderOpen as FolderOpenIcon } from '@mui/icons-material';
+import {
+ Close as CloseIcon,
+ FolderOpen as FolderOpenIcon
+} from '@mui/icons-material';
import { useTranslation } from 'src/stores/localeStore';
const STORAGE_KEY = 'fossflow_import_hint_dismissed';
@@ -57,18 +60,20 @@ export const ImportHintTooltip = () => {
>
-
+
{t('title')}
-
+
- {t('instructionStart')} {t('menuButton')} {t('instructionMiddle')} {t('openButton')} {t('instructionEnd')}
+ {t('instructionStart')} {t('menuButton')}{' '}
+ {t('instructionMiddle')} {t('openButton')}{' '}
+ {t('instructionEnd')}
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/ItemControls/IconSelectionControls/IconGrid.tsx b/packages/fossflow-lib/src/components/ItemControls/IconSelectionControls/IconGrid.tsx
index 8eac0af6..b4b6c62d 100644
--- a/packages/fossflow-lib/src/components/ItemControls/IconSelectionControls/IconGrid.tsx
+++ b/packages/fossflow-lib/src/components/ItemControls/IconSelectionControls/IconGrid.tsx
@@ -12,7 +12,14 @@ interface Props {
onHover?: (index: number) => void;
}
-export const IconGrid = ({ icons, onMouseDown, onClick, onDoubleClick, hoveredIndex, onHover }: Props) => {
+export const IconGrid = ({
+ icons,
+ onMouseDown,
+ onClick,
+ onDoubleClick,
+ hoveredIndex,
+ onHover
+}: Props) => {
return (
{icons.map((icon, index) => {
@@ -25,7 +32,9 @@ export const IconGrid = ({ icons, onMouseDown, onClick, onDoubleClick, hoveredIn
borderRadius: 1,
transition: 'background-color 0.2s'
}}
- onMouseEnter={() => onHover?.(index)}
+ onMouseEnter={() => {
+ return onHover?.(index);
+ }}
>
{
const mode = useUiStateStore((state) => {
return state.mode;
});
- const iconCategoriesState = useUiStateStore((state) => state.iconCategoriesState);
- const modelActions = useModelStore((state) => state.actions);
- const currentIcons = useModelStore((state) => state.icons);
+ const iconCategoriesState = useUiStateStore((state) => {
+ return state.iconCategoriesState;
+ });
+ const modelActions = useModelStore((state) => {
+ return state.actions;
+ });
+ const currentIcons = useModelStore((state) => {
+ return state.icons;
+ });
const { setFilter, filteredIcons, filter } = useIconFiltering();
const { iconCategories } = useIconCategories();
const fileInputRef = useRef(null);
@@ -33,7 +52,6 @@ export const IconSelectionControls = () => {
return localStorage.getItem('fossflow-show-drag-hint') !== 'false';
});
-
const onMouseDown = useCallback(
(icon: Icon) => {
if (mode.type !== 'PLACE_ICON') return;
@@ -56,123 +74,144 @@ export const IconSelectionControls = () => {
localStorage.setItem('fossflow-show-drag-hint', 'false');
}, []);
- const handleFileSelect = useCallback(async (event: React.ChangeEvent) => {
- const files = event.target.files;
- if (!files || files.length === 0) return;
+ const handleFileSelect = useCallback(
+ async (event: React.ChangeEvent) => {
+ const files = event.target.files;
+ if (!files || files.length === 0) return;
- const newIcons: Icon[] = [];
- const existingNames = new Set(currentIcons.map(icon => icon.name.toLowerCase()));
+ const newIcons: Icon[] = [];
+ const existingNames = new Set(
+ currentIcons.map((icon) => {
+ return icon.name.toLowerCase();
+ })
+ );
- for (let i = 0; i < files.length; i++) {
- const file = files[i];
-
- // Check if file is an image
- if (!file.type.startsWith('image/')) {
- console.warn(`Skipping non-image file: ${file.name}`);
- continue;
- }
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
- // Generate unique name
- let baseName = file.name.replace(/\.[^/.]+$/, ''); // Remove extension
- let finalName = baseName;
- let counter = 1;
-
- while (existingNames.has(finalName.toLowerCase())) {
- finalName = `${baseName}_${counter}`;
- counter++;
- }
-
- existingNames.add(finalName.toLowerCase());
+ // Check if file is an image
+ if (!file.type.startsWith('image/')) {
+ console.warn(`Skipping non-image file: ${file.name}`);
+ continue;
+ }
+
+ // Generate unique name
+ const baseName = file.name.replace(/\.[^/.]+$/, ''); // Remove extension
+ let finalName = baseName;
+ let counter = 1;
+
+ while (existingNames.has(finalName.toLowerCase())) {
+ finalName = `${baseName}_${counter}`;
+ counter++;
+ }
- // Load and scale the image
- const dataUrl = await new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = async (e) => {
- const originalDataUrl = e.target?.result as string;
-
- // For SVG files, use as-is since they scale naturally
- if (file.type === 'image/svg+xml') {
- resolve(originalDataUrl);
- return;
- }
-
- // For raster images, scale them to fit in a square bounding box
- const img = new Image();
- img.onload = () => {
- // Create canvas for scaling
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
- if (!ctx) {
- resolve(originalDataUrl); // Fallback to original
+ existingNames.add(finalName.toLowerCase());
+
+ // Load and scale the image
+ const dataUrl = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = async (e) => {
+ const originalDataUrl = e.target?.result as string;
+
+ // For SVG files, use as-is since they scale naturally
+ if (file.type === 'image/svg+xml') {
+ resolve(originalDataUrl);
return;
}
-
- // Use a square target size for consistent display
- // This ensures all icons have the same bounding box
- const TARGET_SIZE = 128; // Square size for consistency
-
- // Calculate scaling to fit within square while maintaining aspect ratio
- const basScale = Math.min(TARGET_SIZE / img.width, TARGET_SIZE / img.height);
- // Apply user's custom scaling
- const finalScale = basScale * (iconScale / 100);
- const scaledWidth = img.width * finalScale;
- const scaledHeight = img.height * finalScale;
-
- // Set canvas to square size
- canvas.width = TARGET_SIZE;
- canvas.height = TARGET_SIZE;
-
- // Clear canvas with transparent background
- ctx.clearRect(0, 0, TARGET_SIZE, TARGET_SIZE);
-
- // Calculate position to center the image in the square
- const x = (TARGET_SIZE - scaledWidth) / 2;
- const y = (TARGET_SIZE - scaledHeight) / 2;
-
- // Enable image smoothing for better quality
- ctx.imageSmoothingEnabled = true;
- ctx.imageSmoothingQuality = 'high';
-
- // Draw scaled and centered image
- ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
-
- // Convert to data URL (using PNG for transparency)
- resolve(canvas.toDataURL('image/png'));
+
+ // For raster images, scale them to fit in a square bounding box
+ const img = new Image();
+ img.onload = () => {
+ // Create canvas for scaling
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ resolve(originalDataUrl); // Fallback to original
+ return;
+ }
+
+ // Use a square target size for consistent display
+ // This ensures all icons have the same bounding box
+ const TARGET_SIZE = 128; // Square size for consistency
+
+ // Calculate scaling to fit within square while maintaining aspect ratio
+ const basScale = Math.min(
+ TARGET_SIZE / img.width,
+ TARGET_SIZE / img.height
+ );
+ // Apply user's custom scaling
+ const finalScale = basScale * (iconScale / 100);
+ const scaledWidth = img.width * finalScale;
+ const scaledHeight = img.height * finalScale;
+
+ // Set canvas to square size
+ canvas.width = TARGET_SIZE;
+ canvas.height = TARGET_SIZE;
+
+ // Clear canvas with transparent background
+ ctx.clearRect(0, 0, TARGET_SIZE, TARGET_SIZE);
+
+ // Calculate position to center the image in the square
+ const x = (TARGET_SIZE - scaledWidth) / 2;
+ const y = (TARGET_SIZE - scaledHeight) / 2;
+
+ // Enable image smoothing for better quality
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = 'high';
+
+ // Draw scaled and centered image
+ ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
+
+ // Convert to data URL (using PNG for transparency)
+ resolve(canvas.toDataURL('image/png'));
+ };
+ img.onerror = () => {
+ return reject(new Error('Failed to load image'));
+ };
+ img.src = originalDataUrl;
};
- img.onerror = () => reject(new Error('Failed to load image'));
- img.src = originalDataUrl;
- };
- reader.onerror = reject;
- reader.readAsDataURL(file);
- });
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
- newIcons.push({
- id: generateId(),
- name: finalName,
- url: dataUrl,
- collection: 'imported',
- isIsometric: treatAsIsometric // Use user's preference
- });
- }
+ newIcons.push({
+ id: generateId(),
+ name: finalName,
+ url: dataUrl,
+ collection: 'imported',
+ isIsometric: treatAsIsometric // Use user's preference
+ });
+ }
- if (newIcons.length > 0) {
- // Add new icons to the model
- const updatedIcons = [...currentIcons, ...newIcons];
- modelActions.set({ icons: updatedIcons });
-
- // Update icon categories to include imported collection
- const hasImported = iconCategoriesState.some(cat => cat.id === 'imported');
- if (!hasImported) {
- uiStateActions.setIconCategoriesState([
- ...iconCategoriesState,
- { id: 'imported', isExpanded: true }
- ]);
+ if (newIcons.length > 0) {
+ // Add new icons to the model
+ const updatedIcons = [...currentIcons, ...newIcons];
+ modelActions.set({ icons: updatedIcons });
+
+ // Update icon categories to include imported collection
+ const hasImported = iconCategoriesState.some((cat) => {
+ return cat.id === 'imported';
+ });
+ if (!hasImported) {
+ uiStateActions.setIconCategoriesState([
+ ...iconCategoriesState,
+ { id: 'imported', isExpanded: true }
+ ]);
+ }
}
- }
- // Reset input
- event.target.value = '';
- }, [currentIcons, modelActions, iconCategoriesState, uiStateActions, treatAsIsometric, iconScale]);
+ // Reset input
+ event.target.value = '';
+ },
+ [
+ currentIcons,
+ modelActions,
+ iconCategoriesState,
+ uiStateActions,
+ treatAsIsometric,
+ iconScale
+ ]
+ );
return (
{
{!filteredIcons && (
)}
-
+
-
+
}
@@ -240,7 +281,9 @@ export const IconSelectionControls = () => {
control={
setTreatAsIsometric(e.target.checked)}
+ onChange={(e) => {
+ return setTreatAsIsometric(e.target.checked);
+ }}
size="small"
/>
}
@@ -251,11 +294,15 @@ export const IconSelectionControls = () => {
}
sx={{ mt: 1, ml: 0 }}
/>
-
+
Uncheck for flat icons (logos, UI elements)
-
+
{
style={{ display: 'none' }}
onChange={handleFileSelect}
/>
-
+
{showAlert && (
-
diff --git a/packages/fossflow-lib/src/components/ItemControls/NodeControls/NodeSettings/NodeSettings.tsx b/packages/fossflow-lib/src/components/ItemControls/NodeControls/NodeSettings/NodeSettings.tsx
index 3adef718..530f7bca 100644
--- a/packages/fossflow-lib/src/components/ItemControls/NodeControls/NodeSettings/NodeSettings.tsx
+++ b/packages/fossflow-lib/src/components/ItemControls/NodeControls/NodeSettings/NodeSettings.tsx
@@ -26,11 +26,17 @@ export const NodeSettings = ({
onDeleted
}: Props) => {
const modelItem = useModelItem(node.id);
- const modelActions = useModelStore((state) => state.actions);
- const icons = useModelStore((state) => state.icons);
-
+ const modelActions = useModelStore((state) => {
+ return state.actions;
+ });
+ const icons = useModelStore((state) => {
+ return state.icons;
+ });
+
// Local state for smooth slider interaction
- const currentIcon = icons.find(icon => icon.id === modelItem?.icon);
+ const currentIcon = icons.find((icon) => {
+ return icon.id === modelItem?.icon;
+ });
const [localScale, setLocalScale] = useState(currentIcon?.scale || 1);
const debounceRef = useRef();
@@ -40,27 +46,31 @@ export const NodeSettings = ({
}, [currentIcon?.scale]);
// Debounced update to store
- const updateIconScale = useCallback((scale: number) => {
- if (debounceRef.current) {
- clearTimeout(debounceRef.current);
- }
-
- debounceRef.current = setTimeout(() => {
- const updatedIcons = icons.map(icon =>
- icon.id === modelItem?.icon
- ? { ...icon, scale }
- : icon
- );
- modelActions.set({ icons: updatedIcons });
- }, 100); // 100ms debounce
- }, [icons, modelItem?.icon, modelActions]);
+ const updateIconScale = useCallback(
+ (scale: number) => {
+ if (debounceRef.current) {
+ clearTimeout(debounceRef.current);
+ }
+
+ debounceRef.current = setTimeout(() => {
+ const updatedIcons = icons.map((icon) => {
+ return icon.id === modelItem?.icon ? { ...icon, scale } : icon;
+ });
+ modelActions.set({ icons: updatedIcons });
+ }, 100); // 100ms debounce
+ },
+ [icons, modelItem?.icon, modelActions]
+ );
// Handle slider change with local state + debounced store update
- const handleScaleChange = useCallback((e: Event, newScale: number | number[]) => {
- const scale = newScale as number;
- setLocalScale(scale); // Immediate UI update
- updateIconScale(scale); // Debounced store update
- }, [updateIconScale]);
+ const handleScaleChange = useCallback(
+ (e: Event, newScale: number | number[]) => {
+ const scale = newScale as number;
+ setLocalScale(scale); // Immediate UI update
+ updateIconScale(scale); // Debounced store update
+ },
+ [updateIconScale]
+ );
// Cleanup timeout on unmount
useEffect(() => {
diff --git a/packages/fossflow-lib/src/components/ItemControls/NodeControls/QuickIconSelector.tsx b/packages/fossflow-lib/src/components/ItemControls/NodeControls/QuickIconSelector.tsx
index 1393bb2e..6ccfba56 100644
--- a/packages/fossflow-lib/src/components/ItemControls/NodeControls/QuickIconSelector.tsx
+++ b/packages/fossflow-lib/src/components/ItemControls/NodeControls/QuickIconSelector.tsx
@@ -1,5 +1,19 @@
-import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
-import { Box, Stack, Typography, Divider, TextField, InputAdornment, Alert } from '@mui/material';
+import React, {
+ useState,
+ useEffect,
+ useRef,
+ useMemo,
+ useCallback
+} from 'react';
+import {
+ Box,
+ Stack,
+ Typography,
+ Divider,
+ TextField,
+ InputAdornment,
+ Alert
+} from '@mui/material';
import { Search as SearchIcon } from '@mui/icons-material';
import { Icon } from 'src/types';
import { useModelStore } from 'src/stores/modelStore';
@@ -30,7 +44,9 @@ const getRecentIcons = (): string[] => {
const addToRecentIcons = (iconId: string) => {
const recent = getRecentIcons();
// Remove if already exists and add to front
- const filtered = recent.filter(id => id !== iconId);
+ const filtered = recent.filter((id) => {
+ return id !== iconId;
+ });
const updated = [iconId, ...filtered].slice(0, MAX_RECENT_ICONS);
localStorage.setItem(RECENT_ICONS_KEY, JSON.stringify(updated));
};
@@ -40,35 +56,51 @@ const escapeRegex = (str: string): string => {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
-export const QuickIconSelector = ({ onIconSelected, onClose, currentIconId }: Props) => {
+export const QuickIconSelector = ({
+ onIconSelected,
+ onClose,
+ currentIconId
+}: Props) => {
const [searchTerm, setSearchTerm] = useState('');
const [hoveredIndex, setHoveredIndex] = useState(0);
const searchInputRef = useRef(null);
-
- const icons = useModelStore((state) => state.icons);
+
+ const icons = useModelStore((state) => {
+ return state.icons;
+ });
const { iconCategories } = useIconCategories();
// Get recently used icons
- const recentIconIds = useMemo(() => getRecentIcons(), []);
+ const recentIconIds = useMemo(() => {
+ return getRecentIcons();
+ }, []);
const recentIcons = useMemo(() => {
return recentIconIds
- .map(id => icons.find(icon => icon.id === id))
+ .map((id) => {
+ return icons.find((icon) => {
+ return icon.id === id;
+ });
+ })
.filter(Boolean) as Icon[];
}, [recentIconIds, icons]);
// Filter icons based on search
const filteredIcons = useMemo(() => {
if (!searchTerm) return null;
-
+
try {
// Escape special regex characters to prevent errors
const escapedSearch = escapeRegex(searchTerm);
const regex = new RegExp(escapedSearch, 'gi');
- return icons.filter(icon => regex.test(icon.name));
+ return icons.filter((icon) => {
+ return regex.test(icon.name);
+ });
} catch (e) {
// If regex still fails somehow, fall back to simple includes
const lowerSearch = searchTerm.toLowerCase();
- return icons.filter(icon => icon.name.toLowerCase().includes(lowerSearch));
+ return icons.filter((icon) => {
+ return icon.name.toLowerCase().includes(lowerSearch);
+ });
}
}, [searchTerm, icons]);
@@ -82,34 +114,34 @@ export const QuickIconSelector = ({ onIconSelected, onClose, currentIconId }: Pr
const handleKeyDown = (e: KeyboardEvent) => {
// Only handle navigation if we're showing search results
if (!filteredIcons || filteredIcons.length === 0) return;
-
+
const itemsPerRow = 4; // Adjust based on your grid layout
const totalItems = filteredIcons.length;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
- setHoveredIndex(prev =>
- Math.min(prev + itemsPerRow, totalItems - 1)
- );
+ setHoveredIndex((prev) => {
+ return Math.min(prev + itemsPerRow, totalItems - 1);
+ });
break;
case 'ArrowUp':
e.preventDefault();
- setHoveredIndex(prev =>
- Math.max(prev - itemsPerRow, 0)
- );
+ setHoveredIndex((prev) => {
+ return Math.max(prev - itemsPerRow, 0);
+ });
break;
case 'ArrowLeft':
e.preventDefault();
- setHoveredIndex(prev =>
- prev > 0 ? prev - 1 : prev
- );
+ setHoveredIndex((prev) => {
+ return prev > 0 ? prev - 1 : prev;
+ });
break;
case 'ArrowRight':
e.preventDefault();
- setHoveredIndex(prev =>
- prev < totalItems - 1 ? prev + 1 : prev
- );
+ setHoveredIndex((prev) => {
+ return prev < totalItems - 1 ? prev + 1 : prev;
+ });
break;
case 'Enter':
e.preventDefault();
@@ -125,18 +157,26 @@ export const QuickIconSelector = ({ onIconSelected, onClose, currentIconId }: Pr
};
window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
+ return () => {
+ return window.removeEventListener('keydown', handleKeyDown);
+ };
}, [filteredIcons, hoveredIndex, onClose]);
- const handleIconSelect = useCallback((icon: Icon) => {
- addToRecentIcons(icon.id);
- onIconSelected(icon);
- }, [onIconSelected]);
+ const handleIconSelect = useCallback(
+ (icon: Icon) => {
+ addToRecentIcons(icon.id);
+ onIconSelected(icon);
+ },
+ [onIconSelected]
+ );
- const handleIconDoubleClick = useCallback((icon: Icon) => {
- handleIconSelect(icon);
- onClose?.();
- }, [handleIconSelect, onClose]);
+ const handleIconDoubleClick = useCallback(
+ (icon: Icon) => {
+ handleIconSelect(icon);
+ onClose?.();
+ },
+ [handleIconSelect, onClose]
+ );
return (
@@ -202,7 +242,9 @@ export const QuickIconSelector = ({ onIconSelected, onClose, currentIconId }: Pr
) : (
- No icons found matching "{searchTerm}"
+
+ No icons found matching "{searchTerm}"
+
)}
@@ -223,12 +265,11 @@ export const QuickIconSelector = ({ onIconSelected, onClose, currentIconId }: Pr
{/* Help Text */}
- {searchTerm
+ {searchTerm
? 'Use arrow keys to navigate • Enter to select • Double-click to select and close'
- : 'Type to search • Click category to expand • Double-click to select and close'
- }
+ : 'Type to search • Click category to expand • Double-click to select and close'}
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/ItemControls/RectangleControls/RectangleControls.tsx b/packages/fossflow-lib/src/components/ItemControls/RectangleControls/RectangleControls.tsx
index fd84b0d1..06137f46 100644
--- a/packages/fossflow-lib/src/components/ItemControls/RectangleControls/RectangleControls.tsx
+++ b/packages/fossflow-lib/src/components/ItemControls/RectangleControls/RectangleControls.tsx
@@ -1,5 +1,11 @@
import React, { useState } from 'react';
-import { Box, IconButton as MUIIconButton, FormControlLabel, Switch, Typography } from '@mui/material';
+import {
+ Box,
+ IconButton as MUIIconButton,
+ FormControlLabel,
+ Switch,
+ Typography
+} from '@mui/material';
import { useRectangle } from 'src/hooks/useRectangle';
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
import { ColorPicker } from 'src/components/ColorSelector/ColorPicker';
@@ -21,7 +27,9 @@ export const RectangleControls = ({ id }: Props) => {
});
const rectangle = useRectangle(id);
const { updateRectangle, deleteRectangle } = useScene();
- const [useCustomColor, setUseCustomColor] = useState(!!rectangle?.customColor);
+ const [useCustomColor, setUseCustomColor] = useState(
+ !!rectangle?.customColor
+ );
// If rectangle doesn't exist, return null
if (!rectangle) {
diff --git a/packages/fossflow-lib/src/components/Label/ExpandableLabel.tsx b/packages/fossflow-lib/src/components/Label/ExpandableLabel.tsx
index ebeb9e38..21d46312 100644
--- a/packages/fossflow-lib/src/components/Label/ExpandableLabel.tsx
+++ b/packages/fossflow-lib/src/components/Label/ExpandableLabel.tsx
@@ -17,9 +17,15 @@ export const ExpandableLabel = ({
onToggleExpand,
...rest
}: Props) => {
- const forceExpandLabels = useUiStateStore((state) => state.expandLabels);
- const editorMode = useUiStateStore((state) => state.editorMode);
- const labelSettings = useUiStateStore((state) => state.labelSettings);
+ const forceExpandLabels = useUiStateStore((state) => {
+ return state.expandLabels;
+ });
+ const editorMode = useUiStateStore((state) => {
+ return state.editorMode;
+ });
+ const labelSettings = useUiStateStore((state) => {
+ return state.labelSettings;
+ });
const [isExpanded, setIsExpanded] = useState(false);
const contentRef = useRef();
const { observe, size: contentSize } = useResizeObserver();
@@ -32,7 +38,8 @@ export const ExpandableLabel = ({
const effectiveExpanded = useMemo(() => {
// Only force expand in NON_INTERACTIVE mode (export preview)
- const shouldForceExpand = forceExpandLabels && editorMode === 'NON_INTERACTIVE';
+ const shouldForceExpand =
+ forceExpandLabels && editorMode === 'NON_INTERACTIVE';
return shouldForceExpand || isExpanded;
}, [forceExpandLabels, isExpanded, editorMode]);
@@ -41,7 +48,9 @@ export const ExpandableLabel = ({
}, [effectiveExpanded]);
const isContentTruncated = useMemo(() => {
- return !effectiveExpanded && contentSize.height >= STANDARD_LABEL_HEIGHT - 10;
+ return (
+ !effectiveExpanded && contentSize.height >= STANDARD_LABEL_HEIGHT - 10
+ );
}, [effectiveExpanded, contentSize.height]);
// Determine overflow behavior based on mode
@@ -70,7 +79,10 @@ export const ExpandableLabel = ({
'&::-webkit-scrollbar': {
display: 'none'
},
- pb: isContentTruncated || isExpanded ? labelSettings.expandButtonPadding : 0 // Add bottom padding when expand button is visible
+ pb:
+ isContentTruncated || isExpanded
+ ? labelSettings.expandButtonPadding
+ : 0 // Add bottom padding when expand button is visible
}}
style={{
overflowY: overflowBehavior,
@@ -92,21 +104,22 @@ export const ExpandableLabel = ({
)}
- {editorMode !== 'NON_INTERACTIVE' && ((!isExpanded && isContentTruncated) || isExpanded) && (
- {
- setIsExpanded(!isExpanded);
- onToggleExpand?.(!isExpanded);
- }}
- />
- )}
+ {editorMode !== 'NON_INTERACTIVE' &&
+ ((!isExpanded && isContentTruncated) || isExpanded) && (
+ {
+ setIsExpanded(!isExpanded);
+ onToggleExpand?.(!isExpanded);
+ }}
+ />
+ )}
);
};
diff --git a/packages/fossflow-lib/src/components/LabelSettings/LabelSettings.tsx b/packages/fossflow-lib/src/components/LabelSettings/LabelSettings.tsx
index 8884b661..849993f4 100644
--- a/packages/fossflow-lib/src/components/LabelSettings/LabelSettings.tsx
+++ b/packages/fossflow-lib/src/components/LabelSettings/LabelSettings.tsx
@@ -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({
@@ -27,7 +27,11 @@ export const LabelSettings = () => {
Expand Button Padding
-
+
Bottom padding when expand button is visible (prevents text overlap)
{
const { t } = useTranslation('lassoHintTooltip');
const theme = useTheme();
- const mode = useUiStateStore((state) => state.mode);
+ const mode = useUiStateStore((state) => {
+ return state.mode;
+ });
const [isDismissed, setIsDismissed] = useState(true);
const [position, setPosition] = useState({ top: 16, right: 16 });
@@ -50,7 +52,10 @@ export const LassoHintTooltip = ({ toolMenuRef }: Props) => {
};
// Only show when in LASSO or FREEHAND_LASSO mode
- if (isDismissed || (mode.type !== 'LASSO' && mode.type !== 'FREEHAND_LASSO')) {
+ if (
+ isDismissed ||
+ (mode.type !== 'LASSO' && mode.type !== 'FREEHAND_LASSO')
+ ) {
return null;
}
@@ -95,7 +100,9 @@ export const LassoHintTooltip = ({ toolMenuRef }: Props) => {
{isFreehandMode ? (
<>
- {t('freehandDragStart')} {t('freehandDragMiddle')} {t('freehandDragEnd')} {t('freehandComplete')}
+ {t('freehandDragStart')}{' '}
+ {t('freehandDragMiddle')} {t('freehandDragEnd')}{' '}
+ {t('freehandComplete')}
>
) : (
<>
diff --git a/packages/fossflow-lib/src/components/LazyLoadingWelcomeNotification/LazyLoadingWelcomeNotification.tsx b/packages/fossflow-lib/src/components/LazyLoadingWelcomeNotification/LazyLoadingWelcomeNotification.tsx
index 5f000afc..c7ed92cc 100644
--- a/packages/fossflow-lib/src/components/LazyLoadingWelcomeNotification/LazyLoadingWelcomeNotification.tsx
+++ b/packages/fossflow-lib/src/components/LazyLoadingWelcomeNotification/LazyLoadingWelcomeNotification.tsx
@@ -70,15 +70,17 @@ export const LazyLoadingWelcomeNotification = () => {
{t('message')}
-
+
{t('configPath')} {t('configPath2')}
diff --git a/packages/fossflow-lib/src/components/MainMenu/MainMenu.tsx b/packages/fossflow-lib/src/components/MainMenu/MainMenu.tsx
index dab16bb1..514ce355 100644
--- a/packages/fossflow-lib/src/components/MainMenu/MainMenu.tsx
+++ b/packages/fossflow-lib/src/components/MainMenu/MainMenu.tsx
@@ -9,8 +9,7 @@ import {
DeleteOutline as DeleteOutlineIcon,
Undo as UndoIcon,
Redo as RedoIcon,
- Settings as SettingsIcon,
-
+ Settings as SettingsIcon
} from '@mui/icons-material';
import { UiElement } from 'src/components/UiElement/UiElement';
import { IconButton } from 'src/components/IconButton/IconButton';
@@ -134,9 +133,6 @@ export const MainMenu = () => {
uiStateActions.setDialog(DialogTypeEnum.SETTINGS);
}, [uiStateActions]);
-
-
-
const sectionVisibility = useMemo(() => {
return {
actions: Boolean(
@@ -201,7 +197,6 @@ export const MainMenu = () => {
{t('redo')}
-
{(canUndo || canRedo) && sectionVisibility.actions && }
{/* File Actions */}
diff --git a/packages/fossflow-lib/src/components/PanSettings/PanSettings.tsx b/packages/fossflow-lib/src/components/PanSettings/PanSettings.tsx
index 64a3204f..b824408e 100644
--- a/packages/fossflow-lib/src/components/PanSettings/PanSettings.tsx
+++ b/packages/fossflow-lib/src/components/PanSettings/PanSettings.tsx
@@ -12,8 +12,12 @@ import { useUiStateStore } from 'src/stores/uiStateStore';
import { useTranslation } from 'src/stores/localeStore';
export const PanSettings = () => {
- const panSettings = useUiStateStore((state) => state.panSettings);
- const setPanSettings = useUiStateStore((state) => state.actions.setPanSettings);
+ const panSettings = useUiStateStore((state) => {
+ return state.panSettings;
+ });
+ const setPanSettings = useUiStateStore((state) => {
+ return state.actions.setPanSettings;
+ });
const { t } = useTranslation();
const handleToggle = (setting: keyof typeof panSettings) => {
@@ -47,7 +51,9 @@ export const PanSettings = () => {
control={
handleToggle('emptyAreaClickPan')}
+ onChange={() => {
+ return handleToggle('emptyAreaClickPan');
+ }}
/>
}
label={t('settings.pan.emptyAreaClickPan')}
@@ -57,7 +63,9 @@ export const PanSettings = () => {
control={
handleToggle('middleClickPan')}
+ onChange={() => {
+ return handleToggle('middleClickPan');
+ }}
/>
}
label={t('settings.pan.middleClickPan')}
@@ -67,7 +75,9 @@ export const PanSettings = () => {
control={
handleToggle('rightClickPan')}
+ onChange={() => {
+ return handleToggle('rightClickPan');
+ }}
/>
}
label={t('settings.pan.rightClickPan')}
@@ -77,7 +87,9 @@ export const PanSettings = () => {
control={
handleToggle('ctrlClickPan')}
+ onChange={() => {
+ return handleToggle('ctrlClickPan');
+ }}
/>
}
label={t('settings.pan.ctrlClickPan')}
@@ -87,7 +99,9 @@ export const PanSettings = () => {
control={
handleToggle('altClickPan')}
+ onChange={() => {
+ return handleToggle('altClickPan');
+ }}
/>
}
label={t('settings.pan.altClickPan')}
@@ -103,7 +117,9 @@ export const PanSettings = () => {
control={
handleToggle('arrowKeysPan')}
+ onChange={() => {
+ return handleToggle('arrowKeysPan');
+ }}
/>
}
label={t('settings.pan.arrowKeys')}
@@ -113,7 +129,9 @@ export const PanSettings = () => {
control={
handleToggle('wasdPan')}
+ onChange={() => {
+ return handleToggle('wasdPan');
+ }}
/>
}
label={t('settings.pan.wasdKeys')}
@@ -123,7 +141,9 @@ export const PanSettings = () => {
control={
handleToggle('ijklPan')}
+ onChange={() => {
+ return handleToggle('ijklPan');
+ }}
/>
}
label={t('settings.pan.ijklKeys')}
@@ -138,7 +158,9 @@ export const PanSettings = () => {
handleSpeedChange(value as number)}
+ onChange={(_, value) => {
+ return handleSpeedChange(value as number);
+ }}
min={5}
max={50}
step={5}
@@ -148,9 +170,13 @@ export const PanSettings = () => {
-
+
{t('settings.pan.note')}
);
-};
\ No newline at end of file
+};
diff --git a/packages/fossflow-lib/src/components/Renderer/Renderer.tsx b/packages/fossflow-lib/src/components/Renderer/Renderer.tsx
index 42ce70ac..47c87aad 100644
--- a/packages/fossflow-lib/src/components/Renderer/Renderer.tsx
+++ b/packages/fossflow-lib/src/components/Renderer/Renderer.tsx
@@ -53,7 +53,11 @@ export const Renderer = ({ showGrid, backgroundColor }: RendererProps) => {
width: '100%',
height: '100%',
zIndex: 0,
- bgcolor: (theme) => backgroundColor === 'transparent' ? 'transparent' : (backgroundColor ?? theme.customVars.customPalette.diagramBg)
+ bgcolor: (theme) => {
+ return backgroundColor === 'transparent'
+ ? 'transparent'
+ : (backgroundColor ?? theme.customVars.customPalette.diagramBg);
+ }
}}
>
diff --git a/packages/fossflow-lib/src/components/RichTextEditor/RichTextEditorErrorBoundary.tsx b/packages/fossflow-lib/src/components/RichTextEditor/RichTextEditorErrorBoundary.tsx
index 9fc3a320..71104e52 100644
--- a/packages/fossflow-lib/src/components/RichTextEditor/RichTextEditorErrorBoundary.tsx
+++ b/packages/fossflow-lib/src/components/RichTextEditor/RichTextEditorErrorBoundary.tsx
@@ -10,7 +10,10 @@ interface ErrorBoundaryState {
errorCount: number;
}
-class RichTextEditorErrorBoundary extends Component {
+class RichTextEditorErrorBoundary extends Component<
+ ErrorBoundaryProps,
+ ErrorBoundaryState
+> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
@@ -19,7 +22,9 @@ class RichTextEditorErrorBoundary extends Component | null {
+ static getDerivedStateFromError(
+ error: Error
+ ): Partial | null {
// Check if this is the specific DOM manipulation error we're trying to handle
if (
error.message.includes('removeChild') ||
@@ -38,18 +43,25 @@ class RichTextEditorErrorBoundary extends Component ({
- errorCount: prevState.errorCount + 1
- }));
+ this.setState((prevState) => {
+ return {
+ errorCount: prevState.errorCount + 1
+ };
+ });
// If we get too many errors in a row, show fallback
if (this.state.errorCount > 3) {
@@ -66,7 +78,10 @@ class RichTextEditorErrorBoundary extends Component 3) {
// If too many errors, show fallback or placeholder
- return this.props.fallback || (
-
- Rich text editor temporarily unavailable
-
+ return (
+ this.props.fallback || (
+
+ Rich text editor temporarily unavailable
+
+ )
);
}
@@ -94,4 +113,4 @@ class RichTextEditorErrorBoundary extends Component {
- const [isFirstRender, setIsFirstRender] = useState(true);
- const elementRef = useRef(null);
+export const SceneLayer = memo(
+ ({ children, order = 0, sx, disableAnimation }: Props) => {
+ const [isFirstRender, setIsFirstRender] = useState(true);
+ const elementRef = useRef(null);
- const scroll = useUiStateStore((state) => {
- return state.scroll;
- });
- const zoom = useUiStateStore((state) => {
- return state.zoom;
- });
+ const scroll = useUiStateStore((state) => {
+ return state.scroll;
+ });
+ const zoom = useUiStateStore((state) => {
+ return state.zoom;
+ });
- useEffect(() => {
- if (!elementRef.current) return;
+ useEffect(() => {
+ if (!elementRef.current) return;
- gsap.to(elementRef.current, {
- duration: disableAnimation || isFirstRender ? 0 : 0.016, // ~1 frame at 60fps for smooth motion
- ease: 'none', // Linear easing for immediate response
- translateX: scroll.position.x,
- translateY: scroll.position.y,
- scale: zoom
- });
+ gsap.to(elementRef.current, {
+ duration: disableAnimation || isFirstRender ? 0 : 0.016, // ~1 frame at 60fps for smooth motion
+ ease: 'none', // Linear easing for immediate response
+ translateX: scroll.position.x,
+ translateY: scroll.position.y,
+ scale: zoom
+ });
- if (isFirstRender) {
- setIsFirstRender(false);
- }
- }, [zoom, scroll, disableAnimation, isFirstRender]);
+ if (isFirstRender) {
+ setIsFirstRender(false);
+ }
+ }, [zoom, scroll, disableAnimation, isFirstRender]);
- return (
-
- {children}
-
- );
-});
+ return (
+
+ {children}
+
+ );
+ }
+);
diff --git a/packages/fossflow-lib/src/components/SceneLayers/ConnectorLabels/ConnectorLabel.tsx b/packages/fossflow-lib/src/components/SceneLayers/ConnectorLabels/ConnectorLabel.tsx
index 8554c225..8afe35aa 100644
--- a/packages/fossflow-lib/src/components/SceneLayers/ConnectorLabels/ConnectorLabel.tsx
+++ b/packages/fossflow-lib/src/components/SceneLayers/ConnectorLabels/ConnectorLabel.tsx
@@ -28,7 +28,6 @@ export const ConnectorLabel = memo(({ connector: sceneConnector }: Props) => {
const labelPositions = useMemo(() => {
if (!connector) return [];
-
return labels
.map((label) => {
const tileIndex = getLabelTileIndex(
diff --git a/packages/fossflow-lib/src/components/SceneLayers/Connectors/Connector.tsx b/packages/fossflow-lib/src/components/SceneLayers/Connectors/Connector.tsx
index dff93949..7facd023 100644
--- a/packages/fossflow-lib/src/components/SceneLayers/Connectors/Connector.tsx
+++ b/packages/fossflow-lib/src/components/SceneLayers/Connectors/Connector.tsx
@@ -18,312 +18,322 @@ interface Props {
isSelected?: boolean;
}
-export const Connector = memo(({ connector: _connector, isSelected }: Props) => {
- const theme = useTheme();
- const predefinedColor = useColor(_connector.color);
- const { currentView } = useScene();
- const connector = useConnector(_connector.id);
-
- if (!connector) {
- return null;
- }
+export const Connector = memo(
+ ({ connector: _connector, isSelected }: Props) => {
+ const theme = useTheme();
+ const predefinedColor = useColor(_connector.color);
+ const { currentView } = useScene();
+ const connector = useConnector(_connector.id);
- // Use custom color if provided, otherwise use predefined color
- const color = connector.customColor
- ? { value: connector.customColor }
- : predefinedColor;
-
- if (!color) {
- return null;
- }
+ if (!connector) {
+ return null;
+ }
- const { css, pxSize } = useIsoProjection({
- ...connector.path.rectangle
- });
-
- const drawOffset = useMemo(() => {
- return {
- x: UNPROJECTED_TILE_SIZE / 2,
- y: UNPROJECTED_TILE_SIZE / 2
- };
- }, []);
-
- const connectorWidthPx = useMemo(() => {
- return (UNPROJECTED_TILE_SIZE / 100) * connector.width;
- }, [connector.width]);
-
- const pathString = useMemo(() => {
- return connector.path.tiles.reduce((acc, tile) => {
- return `${acc} ${tile.x * UNPROJECTED_TILE_SIZE + drawOffset.x},${
- tile.y * UNPROJECTED_TILE_SIZE + drawOffset.y
- }`;
- }, '');
- }, [connector.path.tiles, drawOffset]);
-
- // Create offset paths for double lines
- const offsetPaths = useMemo(() => {
- if (!connector.lineType || connector.lineType === 'SINGLE') return null;
-
- const tiles = connector.path.tiles;
- if (tiles.length < 2) return null;
-
- const offset = connectorWidthPx * 3; // Larger spacing between double lines for visibility
- const path1Points: string[] = [];
- const path2Points: string[] = [];
-
- for (let i = 0; i < tiles.length; i++) {
- const curr = tiles[i];
- let dx = 0, dy = 0;
-
- // Calculate perpendicular offset based on line direction
- if (i > 0 && i < tiles.length - 1) {
- const prev = tiles[i - 1];
- const next = tiles[i + 1];
- const dx1 = curr.x - prev.x;
- const dy1 = curr.y - prev.y;
- const dx2 = next.x - curr.x;
- const dy2 = next.y - curr.y;
-
- // Average direction for smooth corners
- const avgDx = (dx1 + dx2) / 2;
- const avgDy = (dy1 + dy2) / 2;
- const len = Math.sqrt(avgDx * avgDx + avgDy * avgDy) || 1;
-
- // Perpendicular vector
- dx = -avgDy / len;
- dy = avgDx / len;
- } else if (i === 0 && tiles.length > 1) {
- // Start point
- const next = tiles[1];
- const dirX = next.x - curr.x;
- const dirY = next.y - curr.y;
- const len = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
- dx = -dirY / len;
- dy = dirX / len;
- } else if (i === tiles.length - 1 && tiles.length > 1) {
- // End point
- const prev = tiles[i - 1];
- const dirX = curr.x - prev.x;
- const dirY = curr.y - prev.y;
- const len = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
- dx = -dirY / len;
- dy = dirX / len;
- }
-
- const x = curr.x * UNPROJECTED_TILE_SIZE + drawOffset.x;
- const y = curr.y * UNPROJECTED_TILE_SIZE + drawOffset.y;
-
- path1Points.push(`${x + dx * offset},${y + dy * offset}`);
- path2Points.push(`${x - dx * offset},${y - dy * offset}`);
+ // Use custom color if provided, otherwise use predefined color
+ const color = connector.customColor
+ ? { value: connector.customColor }
+ : predefinedColor;
+
+ if (!color) {
+ return null;
}
-
- return {
- path1: path1Points.join(' '),
- path2: path2Points.join(' ')
- };
- }, [connector.path.tiles, connector.lineType, connectorWidthPx, drawOffset]);
- const anchorPositions = useMemo(() => {
- if (!isSelected) return [];
+ const { css, pxSize } = useIsoProjection({
+ ...connector.path.rectangle
+ });
+
+ const drawOffset = useMemo(() => {
+ return {
+ x: UNPROJECTED_TILE_SIZE / 2,
+ y: UNPROJECTED_TILE_SIZE / 2
+ };
+ }, []);
+
+ const connectorWidthPx = useMemo(() => {
+ return (UNPROJECTED_TILE_SIZE / 100) * connector.width;
+ }, [connector.width]);
+
+ const pathString = useMemo(() => {
+ return connector.path.tiles.reduce((acc, tile) => {
+ return `${acc} ${tile.x * UNPROJECTED_TILE_SIZE + drawOffset.x},${
+ tile.y * UNPROJECTED_TILE_SIZE + drawOffset.y
+ }`;
+ }, '');
+ }, [connector.path.tiles, drawOffset]);
+
+ // Create offset paths for double lines
+ const offsetPaths = useMemo(() => {
+ if (!connector.lineType || connector.lineType === 'SINGLE') return null;
+
+ const tiles = connector.path.tiles;
+ if (tiles.length < 2) return null;
- return connector.anchors.map((anchor) => {
- const position = getAnchorTile(anchor, currentView);
+ const offset = connectorWidthPx * 3; // Larger spacing between double lines for visibility
+ const path1Points: string[] = [];
+ const path2Points: string[] = [];
+
+ for (let i = 0; i < tiles.length; i++) {
+ const curr = tiles[i];
+ let dx = 0,
+ dy = 0;
+
+ // Calculate perpendicular offset based on line direction
+ if (i > 0 && i < tiles.length - 1) {
+ const prev = tiles[i - 1];
+ const next = tiles[i + 1];
+ const dx1 = curr.x - prev.x;
+ const dy1 = curr.y - prev.y;
+ const dx2 = next.x - curr.x;
+ const dy2 = next.y - curr.y;
+
+ // Average direction for smooth corners
+ const avgDx = (dx1 + dx2) / 2;
+ const avgDy = (dy1 + dy2) / 2;
+ const len = Math.sqrt(avgDx * avgDx + avgDy * avgDy) || 1;
+
+ // Perpendicular vector
+ dx = -avgDy / len;
+ dy = avgDx / len;
+ } else if (i === 0 && tiles.length > 1) {
+ // Start point
+ const next = tiles[1];
+ const dirX = next.x - curr.x;
+ const dirY = next.y - curr.y;
+ const len = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
+ dx = -dirY / len;
+ dy = dirX / len;
+ } else if (i === tiles.length - 1 && tiles.length > 1) {
+ // End point
+ const prev = tiles[i - 1];
+ const dirX = curr.x - prev.x;
+ const dirY = curr.y - prev.y;
+ const len = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
+ dx = -dirY / len;
+ dy = dirX / len;
+ }
+
+ const x = curr.x * UNPROJECTED_TILE_SIZE + drawOffset.x;
+ const y = curr.y * UNPROJECTED_TILE_SIZE + drawOffset.y;
+
+ path1Points.push(`${x + dx * offset},${y + dy * offset}`);
+ path2Points.push(`${x - dx * offset},${y - dy * offset}`);
+ }
return {
- id: anchor.id,
- x:
- (connector.path.rectangle.from.x - position.x) *
- UNPROJECTED_TILE_SIZE +
- drawOffset.x,
- y:
- (connector.path.rectangle.from.y - position.y) *
- UNPROJECTED_TILE_SIZE +
- drawOffset.y
+ path1: path1Points.join(' '),
+ path2: path2Points.join(' ')
};
- });
- }, [
- currentView,
- connector.path.rectangle,
- connector.anchors,
- drawOffset,
- isSelected
- ]);
-
- const directionIcon = useMemo(() => {
- return getConnectorDirectionIcon(connector.path.tiles);
- }, [connector.path.tiles]);
-
- const strokeDashArray = useMemo(() => {
- switch (connector.style) {
- case 'DASHED':
- return `${connectorWidthPx * 2}, ${connectorWidthPx * 2}`;
- case 'DOTTED':
- return `0, ${connectorWidthPx * 1.8}`;
- case 'SOLID':
- default:
- return 'none';
- }
- }, [connector.style, connectorWidthPx]);
-
- const lineType = connector.lineType || 'SINGLE';
-
- return (
-
-
-
- );
-});
+ )}
+
+
+ );
+ }
+);
diff --git a/packages/fossflow-lib/src/components/SceneLayers/Rectangles/Rectangle.tsx b/packages/fossflow-lib/src/components/SceneLayers/Rectangles/Rectangle.tsx
index 5cf187bd..7adf2c09 100644
--- a/packages/fossflow-lib/src/components/SceneLayers/Rectangles/Rectangle.tsx
+++ b/packages/fossflow-lib/src/components/SceneLayers/Rectangles/Rectangle.tsx
@@ -6,28 +6,28 @@ import { useColor } from 'src/hooks/useColor';
type Props = ReturnType['rectangles'][0];
-export const Rectangle = memo(({ from, to, color: colorId, customColor }: Props) => {
- const predefinedColor = useColor(colorId);
-
- // Use custom color if provided, otherwise use predefined color
- const color = customColor
- ? { value: customColor }
- : predefinedColor;
+export const Rectangle = memo(
+ ({ from, to, color: colorId, customColor }: Props) => {
+ const predefinedColor = useColor(colorId);
- if (!color) {
- return null;
- }
+ // Use custom color if provided, otherwise use predefined color
+ const color = customColor ? { value: customColor } : predefinedColor;
+
+ if (!color) {
+ return null;
+ }
- return (
-
- );
-});
+ return (
+
+ );
+ }
+);
diff --git a/packages/fossflow-lib/src/components/SettingsDialog/SettingsDialog.tsx b/packages/fossflow-lib/src/components/SettingsDialog/SettingsDialog.tsx
index 567510af..1f25287b 100644
--- a/packages/fossflow-lib/src/components/SettingsDialog/SettingsDialog.tsx
+++ b/packages/fossflow-lib/src/components/SettingsDialog/SettingsDialog.tsx
@@ -38,8 +38,12 @@ export interface SettingsDialogProps {
}
export const SettingsDialog = ({ iconPackManager }: SettingsDialogProps) => {
- const dialog = useUiStateStore((state) => state.dialog);
- const setDialog = useUiStateStore((state) => state.actions.setDialog);
+ const dialog = useUiStateStore((state) => {
+ return state.dialog;
+ });
+ const setDialog = useUiStateStore((state) => {
+ return state.actions.setDialog;
+ });
const [tabValue, setTabValue] = useState(0);
const { t } = useTranslation();
@@ -54,12 +58,7 @@ export const SettingsDialog = ({ iconPackManager }: SettingsDialogProps) => {
};
return (
-