From 9847359bc807e57b048d6653327a25607bbae0db Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Wed, 28 May 2025 18:29:03 +0200 Subject: [PATCH 01/10] :wrench: Do not use anonymous components --- eslint.config.mjs | 22 ++++++++++++------- src/settings-renderer/components/App.tsx | 10 +++++++-- .../components/common/Base64IconPicker.tsx | 4 ++-- .../components/common/Button.tsx | 4 ++-- .../components/common/Checkbox.tsx | 4 ++-- .../components/common/ColorButton.tsx | 4 ++-- .../components/common/Dropdown.tsx | 4 ++-- .../components/common/FilePicker.tsx | 4 ++-- .../components/common/GridIconPicker.tsx | 4 ++-- .../components/common/Headerbar.tsx | 4 ++-- .../components/common/IconChooserButton.tsx | 4 ++-- .../components/common/InfoItem.tsx | 4 ++-- .../components/common/MacroPicker.tsx | 4 ++-- .../components/common/Modal.tsx | 4 ++-- .../components/common/Note.tsx | 4 ++-- .../components/common/Popover.tsx | 4 ++-- .../components/common/RandomTip.tsx | 4 ++-- .../components/common/Scrollbox.tsx | 11 ++++++++-- .../components/common/SettingsCheckbox.tsx | 6 +++-- .../components/common/SettingsDropdown.tsx | 6 +++-- .../components/common/SettingsRow.tsx | 5 ++--- .../components/common/SettingsSpinbutton.tsx | 6 +++-- .../components/common/ShortcutPicker.tsx | 4 ++-- .../components/common/Sidebar.tsx | 4 ++-- .../components/common/Spinbutton.tsx | 4 ++-- .../components/common/Swirl.tsx | 10 +++++++-- .../components/common/Tag.tsx | 4 ++-- .../components/common/TagInput.tsx | 4 ++-- .../components/common/TextInput.tsx | 4 ++-- .../components/common/ThemedIcon.tsx | 4 ++-- .../components/dialogs/AboutDialog.tsx | 4 ++-- .../dialogs/GeneralSettingsDialog.tsx | 4 ++-- .../components/dialogs/IntroDialog.tsx | 4 ++-- .../components/dialogs/MenuThemesDialog.tsx | 4 ++-- .../menu-list/CollectionDetails.tsx | 4 ++-- .../components/menu-list/CollectionList.tsx | 4 ++-- .../components/menu-list/MenuList.tsx | 4 ++-- .../components/menu-preview/MenuPreview.tsx | 4 ++-- .../components/menu-preview/PreviewFooter.tsx | 4 ++-- .../components/menu-preview/PreviewHeader.tsx | 4 ++-- .../menu-properties/MenuBehavior.tsx | 4 ++-- .../menu-properties/MenuConditions.tsx | 4 ++-- .../components/menu-properties/Properties.tsx | 4 ++-- .../menu-properties/ScreenAreaPicker.tsx | 8 +++++-- .../menu-properties/WindowPicker.tsx | 10 +++++++-- 45 files changed, 137 insertions(+), 97 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index dddf9f26d..83d825074 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,17 +2,17 @@ // SPDX-License-Identifier: CC0-1.0 /* eslint-disable @typescript-eslint/naming-convention */ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { defineConfig } from 'eslint/config'; +import {includeIgnoreFile} from '@eslint/compat'; import js from '@eslint/js'; -import ts from 'typescript-eslint'; import prettierRecommended from 'eslint-plugin-prettier/recommended'; -import { includeIgnoreFile } from '@eslint/compat'; +import {defineConfig} from 'eslint/config'; import globals from 'globals'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import ts from 'typescript-eslint'; -const filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(filename); +const filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); const gitignorePath = path.resolve(dirname, '.gitignore'); export default defineConfig([ @@ -33,7 +33,13 @@ export default defineConfig([ '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/naming-convention': 'error', + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'function', + format: ['PascalCase', 'camelCase'], + }, + ], '@typescript-eslint/no-unused-vars': [ 'error', { diff --git a/src/settings-renderer/components/App.tsx b/src/settings-renderer/components/App.tsx index 1ca249b25..e78a35fe3 100644 --- a/src/settings-renderer/components/App.tsx +++ b/src/settings-renderer/components/App.tsx @@ -31,7 +31,13 @@ import { Sidebar } from './common'; import * as classes from './App.module.scss'; const cx = classNames.bind(classes); -export default () => { +/** + * This is the main component of the settings dialog. It manages the layout of the + * different components: the menu list on the left, the menu preview in the center, and + * the properties on the right. It also handles global shortcuts for undo and redo, and + * sets the color scheme of the body element based on the user's settings. + */ +export default function App() { const [settingsWindowColorScheme] = useGeneralSetting('settingsWindowColorScheme'); const [settingsWindowFlavor] = useGeneralSetting('settingsWindowFlavor'); @@ -111,4 +117,4 @@ export default () => { /> ); -}; +} diff --git a/src/settings-renderer/components/common/Base64IconPicker.tsx b/src/settings-renderer/components/common/Base64IconPicker.tsx index e41fadab5..8734dea17 100644 --- a/src/settings-renderer/components/common/Base64IconPicker.tsx +++ b/src/settings-renderer/components/common/Base64IconPicker.tsx @@ -30,7 +30,7 @@ interface IProps { * @param props - The properties for the icon picker component. * @returns A textarea element that allows the user to enter a base64 encoded image. */ -export default (props: IProps) => { +export default function Base64IconPicker(props: IProps) { const textareaRef = React.useRef(null); const [value, setValue] = React.useState(''); @@ -87,4 +87,4 @@ export default (props: IProps) => { ].join('\n')} /> ); -}; +} diff --git a/src/settings-renderer/components/common/Button.tsx b/src/settings-renderer/components/common/Button.tsx index 0a3effe5d..b0e94609e 100644 --- a/src/settings-renderer/components/common/Button.tsx +++ b/src/settings-renderer/components/common/Button.tsx @@ -62,7 +62,7 @@ interface IProps { * @param props - The properties for the button component. * @returns A button element. */ -export default (props: IProps) => { +export default function Button(props: IProps) { const className = cx({ button: true, [props.variant || 'secondary']: true, @@ -84,4 +84,4 @@ export default (props: IProps) => { {props.label} ); -}; +} diff --git a/src/settings-renderer/components/common/Checkbox.tsx b/src/settings-renderer/components/common/Checkbox.tsx index 7f3df08bb..d31b04dab 100644 --- a/src/settings-renderer/components/common/Checkbox.tsx +++ b/src/settings-renderer/components/common/Checkbox.tsx @@ -37,7 +37,7 @@ interface IProps { * @param props - The properties for the checkbox component. * @returns A checkbox element. */ -export default (props: IProps) => { +export default function Checkbox(props: IProps) { return ( { /> ); -}; +} diff --git a/src/settings-renderer/components/common/ColorButton.tsx b/src/settings-renderer/components/common/ColorButton.tsx index 4d19bff33..298be5e91 100644 --- a/src/settings-renderer/components/common/ColorButton.tsx +++ b/src/settings-renderer/components/common/ColorButton.tsx @@ -33,7 +33,7 @@ interface IProps { * @param props - The properties for the color button component. * @returns A color button element. */ -export default (props: IProps) => { +export default function ColorButton(props: IProps) { const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [cssColor, setCSSColor] = React.useState(chroma(props.color).css()); const [inputColor, setInputColor] = React.useState(chroma(props.color).css()); @@ -87,4 +87,4 @@ export default (props: IProps) => { ); -}; +} diff --git a/src/settings-renderer/components/common/Dropdown.tsx b/src/settings-renderer/components/common/Dropdown.tsx index 4cbcec17d..28a2715c4 100644 --- a/src/settings-renderer/components/common/Dropdown.tsx +++ b/src/settings-renderer/components/common/Dropdown.tsx @@ -50,7 +50,7 @@ interface IProps { * @param props - The properties for the dropdown component. * @returns A dropdown element. */ -export default (props: IProps) => { +export default function Dropdown(props: IProps) { const invalidSelection = props.options.find((option) => option.value === props.initialValue) === undefined; @@ -78,4 +78,4 @@ export default (props: IProps) => { ); -}; +} diff --git a/src/settings-renderer/components/common/FilePicker.tsx b/src/settings-renderer/components/common/FilePicker.tsx index 6dfcb6251..c72fa0892 100644 --- a/src/settings-renderer/components/common/FilePicker.tsx +++ b/src/settings-renderer/components/common/FilePicker.tsx @@ -42,7 +42,7 @@ interface IProps { * @param props - The properties for the component. * @returns A React component that allows the user to enter a shortcut. */ -export default (props: IProps) => { +export default function FilePicker(props: IProps) { const [path, setPath] = React.useState(props.initialValue); // Update the value when the initialValue prop changes. This is necessary because the @@ -104,4 +104,4 @@ export default (props: IProps) => { ); -}; +} diff --git a/src/settings-renderer/components/common/GridIconPicker.tsx b/src/settings-renderer/components/common/GridIconPicker.tsx index d2bebe7ff..9957cd932 100644 --- a/src/settings-renderer/components/common/GridIconPicker.tsx +++ b/src/settings-renderer/components/common/GridIconPicker.tsx @@ -52,7 +52,7 @@ interface IProps { * @param props - The properties for the icon picker component. * @returns A grid icon picker element. */ -export default (props: IProps) => { +export default function GridIconPicker(props: IProps) { const [gridInstance, setGridInstance] = React.useState(null); const theme = IconThemeRegistry.getInstance().getTheme(props.theme); const fetchedIcons = theme.iconPickerInfo.listIcons(props.filterTerm); @@ -121,4 +121,4 @@ export default (props: IProps) => { ); -}; +} diff --git a/src/settings-renderer/components/common/Headerbar.tsx b/src/settings-renderer/components/common/Headerbar.tsx index bc16e66c0..2f8bfc90b 100644 --- a/src/settings-renderer/components/common/Headerbar.tsx +++ b/src/settings-renderer/components/common/Headerbar.tsx @@ -36,7 +36,7 @@ interface IProps { * @param props - The properties for the header bar component. * @returns A header bar element. */ -export default (props: IProps) => { +export default function Headerbar(props: IProps) { return (
{
); -}; +} diff --git a/src/settings-renderer/components/common/IconChooserButton.tsx b/src/settings-renderer/components/common/IconChooserButton.tsx index f4c441a6e..734afb726 100644 --- a/src/settings-renderer/components/common/IconChooserButton.tsx +++ b/src/settings-renderer/components/common/IconChooserButton.tsx @@ -52,7 +52,7 @@ interface IProps { * @param props - The properties for the color button component. * @returns A color button element. */ -export default (props: IProps) => { +export default function IconChooserButton(props: IProps) { const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [filterTerm, setFilterTerm] = React.useState(''); const [theme, setTheme] = React.useState(props.theme); @@ -144,4 +144,4 @@ export default (props: IProps) => { /> ); -}; +} diff --git a/src/settings-renderer/components/common/InfoItem.tsx b/src/settings-renderer/components/common/InfoItem.tsx index ac264d329..5cf705305 100644 --- a/src/settings-renderer/components/common/InfoItem.tsx +++ b/src/settings-renderer/components/common/InfoItem.tsx @@ -24,7 +24,7 @@ interface IProps { * @param props - The properties for the info item component. * @returns An info item element. */ -export default (props: IProps) => { +export default function InfoItem(props: IProps) { return ( { ); -}; +} diff --git a/src/settings-renderer/components/common/MacroPicker.tsx b/src/settings-renderer/components/common/MacroPicker.tsx index 08e8aa150..b350e6ed4 100644 --- a/src/settings-renderer/components/common/MacroPicker.tsx +++ b/src/settings-renderer/components/common/MacroPicker.tsx @@ -43,7 +43,7 @@ interface IProps { * @param props - The properties for the macro-picker component. * @returns A macro-picker element. */ -export default (props: IProps) => { +export default function MacroPicker(props: IProps) { const [textValue, setTextValue] = React.useState(convertToString(props.initialValue)); const [recording, setRecording] = React.useState(false); const inputRef = React.useRef(null); @@ -127,7 +127,7 @@ export default (props: IProps) => { /> ); -}; +} /** * This method normalizes the given macro. It properly formats the JSON input and turns diff --git a/src/settings-renderer/components/common/Modal.tsx b/src/settings-renderer/components/common/Modal.tsx index 0eeb8ce5d..546bb9fc0 100644 --- a/src/settings-renderer/components/common/Modal.tsx +++ b/src/settings-renderer/components/common/Modal.tsx @@ -65,7 +65,7 @@ interface IProps { * @param props - The properties for the modal component. * @returns A modal element. */ -export default (props: IProps) => { +export default function Modal(props: IProps) { const modalContent = React.useRef(null); const pointerDownOnBackground = React.useRef(false); @@ -177,4 +177,4 @@ export default (props: IProps) => { , document.body ); -}; +} diff --git a/src/settings-renderer/components/common/Note.tsx b/src/settings-renderer/components/common/Note.tsx index d50662089..b595707a7 100644 --- a/src/settings-renderer/components/common/Note.tsx +++ b/src/settings-renderer/components/common/Note.tsx @@ -53,7 +53,7 @@ interface IProps { * @param props - The properties for the note component. * @returns A note element. */ -export default (props: IProps) => { +export default function Note(props: IProps) { const handleLinkClick = (href: string) => { if (props.onLinkClick) { props.onLinkClick(href); // Execute the callback with the link's href @@ -99,4 +99,4 @@ export default (props: IProps) => { )} ); -}; +} diff --git a/src/settings-renderer/components/common/Popover.tsx b/src/settings-renderer/components/common/Popover.tsx index 4ba1d1ddf..c67984ff6 100644 --- a/src/settings-renderer/components/common/Popover.tsx +++ b/src/settings-renderer/components/common/Popover.tsx @@ -46,7 +46,7 @@ interface IProps { * @param props - The properties for the modal component. * @returns A popover element. */ -export default (props: IProps) => { +export default function Popover(props: IProps) { const popoverContent = React.useRef(null); const popoverTriangle = React.useRef(null); const popoverTarget = React.useRef(null); @@ -173,4 +173,4 @@ export default (props: IProps) => { )} ); -}; +} diff --git a/src/settings-renderer/components/common/RandomTip.tsx b/src/settings-renderer/components/common/RandomTip.tsx index 38d5a0356..3ad965396 100644 --- a/src/settings-renderer/components/common/RandomTip.tsx +++ b/src/settings-renderer/components/common/RandomTip.tsx @@ -29,7 +29,7 @@ interface IProps { * @param props - The properties for the tip component. * @returns A note element. */ -export default (props: IProps) => { +export default function RandomTip(props: IProps) { return ( { {props.tips[Math.floor(Math.random() * props.tips.length)]} ); -}; +} diff --git a/src/settings-renderer/components/common/Scrollbox.tsx b/src/settings-renderer/components/common/Scrollbox.tsx index dc7f962d3..550822638 100644 --- a/src/settings-renderer/components/common/Scrollbox.tsx +++ b/src/settings-renderer/components/common/Scrollbox.tsx @@ -19,7 +19,14 @@ interface IProps { hideScrollbar?: boolean; } -export default (props: IProps) => { +/** + * Wraps its children in a scrollable box. The scrollbox has a maximum height and a fixed + * width. If the content exceeds the maximum height, a scrollbar will appear. + * + * @param props - The properties for the scrollbox component. + * @returns A scrollbox element. + */ +export default function Scrollbox(props: IProps) { return (
{
{props.children}
); -}; +} diff --git a/src/settings-renderer/components/common/SettingsCheckbox.tsx b/src/settings-renderer/components/common/SettingsCheckbox.tsx index 9a7d35a0e..a509b6877 100644 --- a/src/settings-renderer/components/common/SettingsCheckbox.tsx +++ b/src/settings-renderer/components/common/SettingsCheckbox.tsx @@ -43,7 +43,9 @@ type BooleanKeys = { * @param props - The properties for the managed checkbox component. * @returns A managed checkbox element. */ -export default >(props: IProps) => { +export default function SettingsCheckbox>( + props: IProps +) { const [state, setState] = useGeneralSetting(props.settingsKey); return ( @@ -55,4 +57,4 @@ export default >(props: IProps) => { disabled={props.disabled} /> ); -}; +} diff --git a/src/settings-renderer/components/common/SettingsDropdown.tsx b/src/settings-renderer/components/common/SettingsDropdown.tsx index d5529a50e..29dc6f0b9 100644 --- a/src/settings-renderer/components/common/SettingsDropdown.tsx +++ b/src/settings-renderer/components/common/SettingsDropdown.tsx @@ -53,7 +53,9 @@ type EnumKeys = { * @param props - The properties for the managed dropdown component. * @returns A managed dropdown element. */ -export default >(props: IProps) => { +export default function SettingsDropdown>( + props: IProps +) { const [state, setState] = useGeneralSetting(props.settingsKey); return ( @@ -68,4 +70,4 @@ export default >(props: IProps) => { maxWidth={props.maxWidth} /> ); -}; +} diff --git a/src/settings-renderer/components/common/SettingsRow.tsx b/src/settings-renderer/components/common/SettingsRow.tsx index 1c8c1a07a..d2d0ee157 100644 --- a/src/settings-renderer/components/common/SettingsRow.tsx +++ b/src/settings-renderer/components/common/SettingsRow.tsx @@ -46,8 +46,7 @@ interface IProps { * @param props - The properties for the settings-row component. * @returns A settings-row element. */ -export default (props: IProps) => { - // eslint-disable-next-line @typescript-eslint/naming-convention +export default function SettingsRow(props: IProps) { const Element = props.labelClickable ? 'label' : 'div'; return ( @@ -74,4 +73,4 @@ export default (props: IProps) => { ); -}; +} diff --git a/src/settings-renderer/components/common/SettingsSpinbutton.tsx b/src/settings-renderer/components/common/SettingsSpinbutton.tsx index 0f4cbd54c..0aa325d30 100644 --- a/src/settings-renderer/components/common/SettingsSpinbutton.tsx +++ b/src/settings-renderer/components/common/SettingsSpinbutton.tsx @@ -55,7 +55,9 @@ type NumberKeys = { * @param props - The properties for the managed spinbutton component. * @returns A managed spinbutton element. */ -export default >(props: IProps) => { +export default function SettingsSpinbutton>( + props: IProps +) { const [state, setState] = useGeneralSetting(props.settingsKey); return ( @@ -71,4 +73,4 @@ export default >(props: IProps) => { step={props.step} /> ); -}; +} diff --git a/src/settings-renderer/components/common/ShortcutPicker.tsx b/src/settings-renderer/components/common/ShortcutPicker.tsx index 4f02ae977..d5d02a4c2 100644 --- a/src/settings-renderer/components/common/ShortcutPicker.tsx +++ b/src/settings-renderer/components/common/ShortcutPicker.tsx @@ -64,7 +64,7 @@ interface IProps { * @param props - The properties for the component. * @returns A React component that allows the user to enter a shortcut. */ -export default (props: IProps) => { +export default function ShortcutPicker(props: IProps) { const [shortcut, setShortcut] = React.useState(props.initialValue); const [recording, setRecording] = React.useState(false); const inputRef = React.useRef(null); @@ -199,7 +199,7 @@ export default (props: IProps) => { ); -}; +} /** * This class is used to record and validate shortcuts using key names. It uses the diff --git a/src/settings-renderer/components/common/Sidebar.tsx b/src/settings-renderer/components/common/Sidebar.tsx index 0d36f0eb7..bf17658dd 100644 --- a/src/settings-renderer/components/common/Sidebar.tsx +++ b/src/settings-renderer/components/common/Sidebar.tsx @@ -31,7 +31,7 @@ interface IProps { * @param props - The properties for the sidebar component. * @returns A sidebar element. */ -export default (props: IProps) => { +export default function Sidebar(props: IProps) { const resizer = React.useRef(null); const sidebar = React.useRef(null); @@ -79,4 +79,4 @@ export default (props: IProps) => { {props.position === 'left' &&
} ); -}; +} diff --git a/src/settings-renderer/components/common/Spinbutton.tsx b/src/settings-renderer/components/common/Spinbutton.tsx index 9831e0af3..19f9caa2f 100644 --- a/src/settings-renderer/components/common/Spinbutton.tsx +++ b/src/settings-renderer/components/common/Spinbutton.tsx @@ -55,7 +55,7 @@ interface IProps { * @param props - The properties for the spinbutton component. * @returns A spinbutton element. */ -export default (props: IProps) => { +export default function Spinbutton(props: IProps) { // We store the value of the spin button internally as a string. This way we can properly // handle empty strings when the user deletes the value. const [value, setValue] = React.useState(props.initialValue.toString()); @@ -138,4 +138,4 @@ export default (props: IProps) => {
); -}; +} diff --git a/src/settings-renderer/components/common/Swirl.tsx b/src/settings-renderer/components/common/Swirl.tsx index 797ab94f2..e0bc93590 100644 --- a/src/settings-renderer/components/common/Swirl.tsx +++ b/src/settings-renderer/components/common/Swirl.tsx @@ -22,7 +22,13 @@ interface IProps { width?: number | string; } -export default (props: IProps) => { +/** + * Swirl component displays a decorative swirl image based on the variant specified. + * + * @param props The properties for the swirl component. + * @returns An image element displaying the selected swirl variant. + */ +export default function Swirl(props: IProps) { const swirls = { swirl1, swirl2, @@ -45,4 +51,4 @@ export default (props: IProps) => { }} /> ); -}; +} diff --git a/src/settings-renderer/components/common/Tag.tsx b/src/settings-renderer/components/common/Tag.tsx index 782e22b01..7fc9b775e 100644 --- a/src/settings-renderer/components/common/Tag.tsx +++ b/src/settings-renderer/components/common/Tag.tsx @@ -29,11 +29,11 @@ interface IProps { * @param props - The properties for the tag component. * @returns A tag element. */ -export default (props: IProps) => { +export default function Tag(props: IProps) { return ( ); -}; +} diff --git a/src/settings-renderer/components/common/TagInput.tsx b/src/settings-renderer/components/common/TagInput.tsx index a628426a8..11700b3f9 100644 --- a/src/settings-renderer/components/common/TagInput.tsx +++ b/src/settings-renderer/components/common/TagInput.tsx @@ -46,7 +46,7 @@ interface IProps { * @param props - The properties for the tag input component. * @returns A tag edit field. */ -export default (props: IProps) => { +export default function TagInput(props: IProps) { const [suggestionsVisible, setSuggestionsVisible] = React.useState(false); const inputRef = React.useRef(null); @@ -138,4 +138,4 @@ export default (props: IProps) => { ); -}; +} diff --git a/src/settings-renderer/components/common/TextInput.tsx b/src/settings-renderer/components/common/TextInput.tsx index 4d3b7505a..230b9b983 100644 --- a/src/settings-renderer/components/common/TextInput.tsx +++ b/src/settings-renderer/components/common/TextInput.tsx @@ -54,7 +54,7 @@ interface IProps { * @param props - The properties for the text-input component. * @returns A text-input element. */ -export default (props: IProps) => { +export default function TextInput(props: IProps) { const [value, setValue] = React.useState(props.initialValue); // Update the value when the initialValue prop changes. This is necessary because the @@ -101,4 +101,4 @@ export default (props: IProps) => { )} ); -}; +} diff --git a/src/settings-renderer/components/common/ThemedIcon.tsx b/src/settings-renderer/components/common/ThemedIcon.tsx index 8181ba9dd..473920c7a 100644 --- a/src/settings-renderer/components/common/ThemedIcon.tsx +++ b/src/settings-renderer/components/common/ThemedIcon.tsx @@ -30,7 +30,7 @@ interface IProps { * @param props - The properties for the icon. * @returns An icon component. */ -export default (props: IProps) => { +export default function ThemedIcon(props: IProps) { const iconRef = React.useRef(null); React.useEffect(() => { @@ -50,4 +50,4 @@ export default (props: IProps) => { className={classes.icon} ref={iconRef}> ); -}; +} diff --git a/src/settings-renderer/components/dialogs/AboutDialog.tsx b/src/settings-renderer/components/dialogs/AboutDialog.tsx index 024d9a753..ce1880335 100644 --- a/src/settings-renderer/components/dialogs/AboutDialog.tsx +++ b/src/settings-renderer/components/dialogs/AboutDialog.tsx @@ -26,7 +26,7 @@ const logo = require('../../../../assets/icons/square-icon.svg'); * This dialog shows information about Kando, including the version number and links to * the release notes and the GitHub repository. It also includes a donation button. */ -export default () => { +export default function AboutDialog() { const aboutDialogVisible = useAppState((state) => state.aboutDialogVisible); const setAboutDialogVisible = useAppState((state) => state.setAboutDialogVisible); @@ -101,4 +101,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/dialogs/GeneralSettingsDialog.tsx b/src/settings-renderer/components/dialogs/GeneralSettingsDialog.tsx index a86e85c9d..fc1184ba6 100644 --- a/src/settings-renderer/components/dialogs/GeneralSettingsDialog.tsx +++ b/src/settings-renderer/components/dialogs/GeneralSettingsDialog.tsx @@ -30,7 +30,7 @@ import { } from '../common'; /** This dialog allows the user to configure some general settings of Kando. */ -export default () => { +export default function GeneralSettingsDialog() { const settingsDialogVisible = useAppState((state) => state.settingsDialogVisible); const setSettingsDialogVisible = useAppState((state) => state.setSettingsDialogVisible); const soundThemes = useAppState((state) => state.soundThemes); @@ -445,4 +445,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/dialogs/IntroDialog.tsx b/src/settings-renderer/components/dialogs/IntroDialog.tsx index f3d58b6af..eacf4e054 100644 --- a/src/settings-renderer/components/dialogs/IntroDialog.tsx +++ b/src/settings-renderer/components/dialogs/IntroDialog.tsx @@ -30,7 +30,7 @@ const cx = classNames.bind(classes); * demonstrate the interaction with Kando. The dialog is shown when the user first starts * Kando, unless the user has disabled it in the settings. */ -export default () => { +export default function IntroDialog() { const introDialogVisible = useAppState((state) => state.introDialogVisible); const setIntroDialogVisible = useAppState((state) => state.setIntroDialogVisible); const [showIntroductionDialog] = useGeneralSetting('showIntroductionDialog'); @@ -255,4 +255,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/dialogs/MenuThemesDialog.tsx b/src/settings-renderer/components/dialogs/MenuThemesDialog.tsx index a73d8f772..83047f7bf 100644 --- a/src/settings-renderer/components/dialogs/MenuThemesDialog.tsx +++ b/src/settings-renderer/components/dialogs/MenuThemesDialog.tsx @@ -53,7 +53,7 @@ const openThemeDirectory = () => { * it. In addition, the user can configure some properties like the theme's accent * colors. */ -export default () => { +export default function MenuThemesDialog() { const themesDialogVisible = useAppState((state) => state.themesDialogVisible); const setThemesDialogVisible = useAppState((state) => state.setThemesDialogVisible); @@ -293,4 +293,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/menu-list/CollectionDetails.tsx b/src/settings-renderer/components/menu-list/CollectionDetails.tsx index f497a2139..9d0499469 100644 --- a/src/settings-renderer/components/menu-list/CollectionDetails.tsx +++ b/src/settings-renderer/components/menu-list/CollectionDetails.tsx @@ -34,7 +34,7 @@ interface IProps { * collection is selected, the details will show the collection name and a set of widgets * for editing the collection's name, icon, and tags. */ -export default (props: IProps) => { +export default function CollectionDetails(props: IProps) { const menuCollections = useMenuSettings((state) => state.collections); const selectedCollection = useAppState((state) => state.selectedCollection); const selectCollection = useAppState((state) => state.selectCollection); @@ -220,4 +220,4 @@ export default (props: IProps) => { )} ); -}; +} diff --git a/src/settings-renderer/components/menu-list/CollectionList.tsx b/src/settings-renderer/components/menu-list/CollectionList.tsx index 8f4deb724..2b0661dc2 100644 --- a/src/settings-renderer/components/menu-list/CollectionList.tsx +++ b/src/settings-renderer/components/menu-list/CollectionList.tsx @@ -37,7 +37,7 @@ interface IRenderedCollection { * In addition, there is show-all-menus button at the top, and an add-new-collection * button at the bottom. They are always there, even if no collection is configured. */ -export default () => { +export default function CollectionList() { const menus = useMenuSettings((state) => state.menus); const editMenu = useMenuSettings((state) => state.editMenu); const collections = useMenuSettings((state) => state.collections); @@ -230,4 +230,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/menu-list/MenuList.tsx b/src/settings-renderer/components/menu-list/MenuList.tsx index 9d6fb1f2d..32ab9fdc0 100644 --- a/src/settings-renderer/components/menu-list/MenuList.tsx +++ b/src/settings-renderer/components/menu-list/MenuList.tsx @@ -52,7 +52,7 @@ interface IRenderedMenu { * * In addition, there is a floating button at the bottom which allows to add a new menu. */ -export default () => { +export default function MenuList() { const menuCollections = useMenuSettings((state) => state.collections); const selectedCollection = useAppState((state) => state.selectedCollection); const backend = useAppState((state) => state.backendInfo); @@ -307,4 +307,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/menu-preview/MenuPreview.tsx b/src/settings-renderer/components/menu-preview/MenuPreview.tsx index 72e11084d..32a4ca002 100644 --- a/src/settings-renderer/components/menu-preview/MenuPreview.tsx +++ b/src/settings-renderer/components/menu-preview/MenuPreview.tsx @@ -52,7 +52,7 @@ interface IRenderedMenuItem { * preview of the currently selected menu where the user can reorder and select menu * items. */ -export default () => { +export default function MenuPreview() { const selectedMenu = useAppState((state) => state.selectedMenu); const selectedChildPath = useAppState((state) => state.selectedChildPath); const selectChildPath = useAppState((state) => state.selectChildPath); @@ -764,4 +764,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/menu-preview/PreviewFooter.tsx b/src/settings-renderer/components/menu-preview/PreviewFooter.tsx index c847e3837..644dfe183 100644 --- a/src/settings-renderer/components/menu-preview/PreviewFooter.tsx +++ b/src/settings-renderer/components/menu-preview/PreviewFooter.tsx @@ -23,7 +23,7 @@ import { ThemedIcon } from '../common'; * This component encapsules the list of item types which can be dragged to the menu * preview. */ -export default () => { +export default function PreviewFooter() { const [dragIndex, setDragIndex] = React.useState(null); const allItemTypes = Array.from(ItemTypeRegistry.getInstance().getAllTypes()); @@ -64,4 +64,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/menu-preview/PreviewHeader.tsx b/src/settings-renderer/components/menu-preview/PreviewHeader.tsx index 62e00b080..8ae38e9a5 100644 --- a/src/settings-renderer/components/menu-preview/PreviewHeader.tsx +++ b/src/settings-renderer/components/menu-preview/PreviewHeader.tsx @@ -20,7 +20,7 @@ import { Headerbar, Button } from '../common'; * This is the toolbar at the top of the menu-preview area. It contains some buttons for * undo/redo, and for opening the settings dialogs. */ -export default () => { +export default function PreviewHeader() { // This will force a re-render whenever the menu settings change. For now, this is // necessary to update the undo/redo buttons. In the future, we might want to make // this more fine-grained, maybe by directly subscribing to the past and future states @@ -89,4 +89,4 @@ export default () => { ); return ; -}; +} diff --git a/src/settings-renderer/components/menu-properties/MenuBehavior.tsx b/src/settings-renderer/components/menu-properties/MenuBehavior.tsx index 41d9f2090..45f8e48cc 100644 --- a/src/settings-renderer/components/menu-properties/MenuBehavior.tsx +++ b/src/settings-renderer/components/menu-properties/MenuBehavior.tsx @@ -15,7 +15,7 @@ import { useAppState, useMenuSettings } from '../../state'; import { Checkbox, Note } from '../common'; /** This component shows the behavior options for the currently selected menu. */ -export default () => { +export default function MenuBehavior() { const menus = useMenuSettings((state) => state.menus); const selectedMenu = useAppState((state) => state.selectedMenu); const editMenu = useMenuSettings((state) => state.editMenu); @@ -63,4 +63,4 @@ export default () => { /> ); -}; +} diff --git a/src/settings-renderer/components/menu-properties/MenuConditions.tsx b/src/settings-renderer/components/menu-properties/MenuConditions.tsx index 1036895e1..03e58e697 100644 --- a/src/settings-renderer/components/menu-properties/MenuConditions.tsx +++ b/src/settings-renderer/components/menu-properties/MenuConditions.tsx @@ -23,7 +23,7 @@ import ScreenAreaPicker from './ScreenAreaPicker'; import WindowPicker from './WindowPicker'; /** This component shows the conditions for displaying the currently selected menu. */ -export default () => { +export default function MenuConditions() { const menus = useMenuSettings((state) => state.menus); const selectedMenu = useAppState((state) => state.selectedMenu); const editMenu = useMenuSettings((state) => state.editMenu); @@ -312,4 +312,4 @@ export default () => { /> ); -}; +} diff --git a/src/settings-renderer/components/menu-properties/Properties.tsx b/src/settings-renderer/components/menu-properties/Properties.tsx index 0a672edf7..24271844e 100644 --- a/src/settings-renderer/components/menu-properties/Properties.tsx +++ b/src/settings-renderer/components/menu-properties/Properties.tsx @@ -33,7 +33,7 @@ import MenuBehavior from './MenuBehavior'; * This component shows the properties of the currently selected menu or menu item on the * right side of the settings dialog. */ -export default () => { +export default function Properties() { const backend = useAppState((state) => state.backendInfo); const menus = useMenuSettings((state) => state.menus); const selectedMenu = useAppState((state) => state.selectedMenu); @@ -226,4 +226,4 @@ export default () => { ); -}; +} diff --git a/src/settings-renderer/components/menu-properties/ScreenAreaPicker.tsx b/src/settings-renderer/components/menu-properties/ScreenAreaPicker.tsx index 5bb14ca8d..d11c26ca0 100644 --- a/src/settings-renderer/components/menu-properties/ScreenAreaPicker.tsx +++ b/src/settings-renderer/components/menu-properties/ScreenAreaPicker.tsx @@ -32,7 +32,11 @@ interface IProps { visible: boolean; } -export default (props: IProps) => { +/** + * This component allows the user to select a screen area by dragging two points on the + * screen. + */ +export default function ScreenAreaPicker(props: IProps) { const [leftTop, setLeftTop] = React.useState(null); const [rightBottom, setRightBottom] = React.useState(null); @@ -155,4 +159,4 @@ export default (props: IProps) => { ); -}; +} diff --git a/src/settings-renderer/components/menu-properties/WindowPicker.tsx b/src/settings-renderer/components/menu-properties/WindowPicker.tsx index 79a9dc1d8..8a7e53fca 100644 --- a/src/settings-renderer/components/menu-properties/WindowPicker.tsx +++ b/src/settings-renderer/components/menu-properties/WindowPicker.tsx @@ -34,7 +34,13 @@ interface IProps { visible: boolean; } -export default (props: IProps) => { +/** + * This component allows the user to select an application name or a window title by + * clicking on a button. The user has to wait for a few seconds during which the + * application or window should be focused. After that, the application name or window + * title is read and returned to the parent component. + */ +export default function WindowPicker(props: IProps) { const timeout = 5; const [value, setValue] = React.useState(null); const [timer, setTimer] = React.useState(timeout + 1); @@ -142,4 +148,4 @@ export default (props: IProps) => { ); -}; +} From 1557b74e9beef6a9bfbe9f372016ab2dbcc9a6da Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Wed, 28 May 2025 18:43:18 +0200 Subject: [PATCH 02/10] :green_heart: Re-enable eslint naming convention rules --- eslint.config.mjs | 30 +++++++++++++++---- .../components/common/SettingsRow.tsx | 1 + 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 83d825074..f6ae6318b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,17 +2,17 @@ // SPDX-License-Identifier: CC0-1.0 /* eslint-disable @typescript-eslint/naming-convention */ -import {includeIgnoreFile} from '@eslint/compat'; +import { includeIgnoreFile } from '@eslint/compat'; import js from '@eslint/js'; import prettierRecommended from 'eslint-plugin-prettier/recommended'; -import {defineConfig} from 'eslint/config'; +import { defineConfig } from 'eslint/config'; import globals from 'globals'; import path from 'node:path'; -import {fileURLToPath} from 'node:url'; +import { fileURLToPath } from 'node:url'; import ts from 'typescript-eslint'; -const filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(filename); +const filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); const gitignorePath = path.resolve(dirname, '.gitignore'); export default defineConfig([ @@ -35,6 +35,26 @@ export default defineConfig([ '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/naming-convention': [ 'error', + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'import', + format: ['camelCase', 'PascalCase'], + }, + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + }, { selector: 'function', format: ['PascalCase', 'camelCase'], diff --git a/src/settings-renderer/components/common/SettingsRow.tsx b/src/settings-renderer/components/common/SettingsRow.tsx index d2d0ee157..47ad1663b 100644 --- a/src/settings-renderer/components/common/SettingsRow.tsx +++ b/src/settings-renderer/components/common/SettingsRow.tsx @@ -47,6 +47,7 @@ interface IProps { * @returns A settings-row element. */ export default function SettingsRow(props: IProps) { + // eslint-disable-next-line @typescript-eslint/naming-convention const Element = props.labelClickable ? 'label' : 'div'; return ( From f572bbe18b0cbed3123976719a785c8500870bd5 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Wed, 28 May 2025 21:08:51 +0200 Subject: [PATCH 03/10] :rocket: Memoize icon theme icons --- .../components/common/GridIconPicker.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/settings-renderer/components/common/GridIconPicker.tsx b/src/settings-renderer/components/common/GridIconPicker.tsx index 9957cd932..5656ae592 100644 --- a/src/settings-renderer/components/common/GridIconPicker.tsx +++ b/src/settings-renderer/components/common/GridIconPicker.tsx @@ -54,8 +54,13 @@ interface IProps { */ export default function GridIconPicker(props: IProps) { const [gridInstance, setGridInstance] = React.useState(null); - const theme = IconThemeRegistry.getInstance().getTheme(props.theme); - const fetchedIcons = theme.iconPickerInfo.listIcons(props.filterTerm); + + // Listing the icons is expensive, so we only do it when the theme or filter term + // changes. + const fetchedIcons = React.useMemo(() => { + const theme = IconThemeRegistry.getInstance().getTheme(props.theme); + return theme.iconPickerInfo.listIcons(props.filterTerm); + }, [props.theme, props.filterTerm]); const columns = 8; const rows = Math.ceil(fetchedIcons.length / columns); From 34f6d0d3b4fd8a6ea5bd330e5f7c0cf243f4a113 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Thu, 29 May 2025 06:30:48 +0200 Subject: [PATCH 04/10] :wrench: Only install react tools during development --- src/main/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index 67b20ed06..a4ac12f96 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -167,7 +167,11 @@ const handleArguments = ( app .whenReady() - .then(() => installExtension(REACT_DEVELOPER_TOOLS)) + .then(() => { + if (process.env.NODE_ENV === 'development') { + return installExtension(REACT_DEVELOPER_TOOLS); + } + }) .then(() => { return i18next.use(i18Backend).init({ lng: app.getLocale(), From 39a530f06721589054aca45a3aefea921190cea7 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Thu, 29 May 2025 06:46:03 +0200 Subject: [PATCH 05/10] :rocket: Do not re-render collection list if menus change --- src/settings-renderer/components/menu-list/CollectionList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings-renderer/components/menu-list/CollectionList.tsx b/src/settings-renderer/components/menu-list/CollectionList.tsx index 2b0661dc2..c3a61bf9d 100644 --- a/src/settings-renderer/components/menu-list/CollectionList.tsx +++ b/src/settings-renderer/components/menu-list/CollectionList.tsx @@ -38,9 +38,8 @@ interface IRenderedCollection { * button at the bottom. They are always there, even if no collection is configured. */ export default function CollectionList() { - const menus = useMenuSettings((state) => state.menus); - const editMenu = useMenuSettings((state) => state.editMenu); const collections = useMenuSettings((state) => state.collections); + const editMenu = useMenuSettings((state) => state.editMenu); const deleteCollection = useMenuSettings((state) => state.deleteCollection); const addCollection = useMenuSettings((state) => state.addCollection); const setCollectionDetailsVisible = useAppState( @@ -151,6 +150,7 @@ export default function CollectionList() { const menuIndex = parseInt( event.dataTransfer.getData('kando/menu-index') ); + const menus = useMenuSettings.getState().menus; const currentTags = menus[menuIndex]?.tags || []; const newTags = [ ...new Set([...currentTags, ...collections[collection.index].tags]), From 76924dfc6e54d83af7694ffc4fd0d2be0b6d0b88 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Thu, 29 May 2025 07:33:31 +0200 Subject: [PATCH 06/10] :rocket: Do not re-render menu list if menus change internally --- .../menu-list/CollectionDetails.tsx | 4 +-- .../components/menu-list/MenuList.tsx | 21 +++++++---- src/settings-renderer/state/index.ts | 2 +- src/settings-renderer/state/menu-settings.ts | 35 ++++++++++++++++++- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/settings-renderer/components/menu-list/CollectionDetails.tsx b/src/settings-renderer/components/menu-list/CollectionDetails.tsx index 9d0499469..3a4560970 100644 --- a/src/settings-renderer/components/menu-list/CollectionDetails.tsx +++ b/src/settings-renderer/components/menu-list/CollectionDetails.tsx @@ -19,7 +19,7 @@ import { RiPencilFill } from 'react-icons/ri'; import * as classes from './CollectionDetails.module.scss'; const cx = classNames.bind(classes); -import { useAppState, useMenuSettings } from '../../state'; +import { useAppState, useMenuSettings, useMappedMenuProperties } from '../../state'; import { Button, IconChooserButton, TagInput } from '../common'; interface IProps { @@ -47,7 +47,7 @@ export default function CollectionDetails(props: IProps) { const menuSearchBarVisible = useAppState((state) => state.menuSearchBarVisible); const setMenuSearchBarVisible = useAppState((state) => state.setMenuSearchBarVisible); - const menus = useMenuSettings((state) => state.menus); + const menus = useMappedMenuProperties((menu) => ({ tags: menu.tags })); const editCollection = useMenuSettings((state) => state.editCollection); const [filterTerm, setFilterTerm] = React.useState(''); diff --git a/src/settings-renderer/components/menu-list/MenuList.tsx b/src/settings-renderer/components/menu-list/MenuList.tsx index 32ab9fdc0..1c4f581eb 100644 --- a/src/settings-renderer/components/menu-list/MenuList.tsx +++ b/src/settings-renderer/components/menu-list/MenuList.tsx @@ -17,7 +17,8 @@ import { TbPlus } from 'react-icons/tb'; import * as classes from './MenuList.module.scss'; const cx = classNames.bind(classes); -import { useAppState, useMenuSettings } from '../../state'; +import { useAppState, useMenuSettings, useMappedMenuProperties } from '../../state'; + import { Scrollbox, ThemedIcon, Swirl, Note, Button } from '../common'; import CollectionDetails from './CollectionDetails'; import { ensureUniqueKeys } from '../../utils'; @@ -53,11 +54,17 @@ interface IRenderedMenu { * In addition, there is a floating button at the bottom which allows to add a new menu. */ export default function MenuList() { + const menus = useMappedMenuProperties((menu) => ({ + name: menu.root.name, + icon: menu.root.icon, + iconTheme: menu.root.iconTheme, + shortcut: menu.shortcut, + shortcutID: menu.shortcutID, + tags: menu.tags, + })); const menuCollections = useMenuSettings((state) => state.collections); const selectedCollection = useAppState((state) => state.selectedCollection); const backend = useAppState((state) => state.backendInfo); - - const menus = useMenuSettings((state) => state.menus); const selectedMenu = useAppState((state) => state.selectedMenu); const selectMenu = useAppState((state) => state.selectMenu); const addMenu = useMenuSettings((state) => state.addMenu); @@ -82,12 +89,12 @@ export default function MenuList() { (backend.supportsShortcuts ? menu.shortcut : menu.shortcutID) || i18next.t('settings.not-bound'); const renderedMenu: IRenderedMenu = { - key: menu.root.name + menu.root.icon + menu.root.iconTheme + shortcut, + key: menu.name + menu.icon + menu.iconTheme + shortcut, index, - name: menu.root.name, + name: menu.name, shortcut, - icon: menu.root.icon, - iconTheme: menu.root.iconTheme, + icon: menu.icon, + iconTheme: menu.iconTheme, }; return renderedMenu; diff --git a/src/settings-renderer/state/index.ts b/src/settings-renderer/state/index.ts index 369e99938..67539b398 100644 --- a/src/settings-renderer/state/index.ts +++ b/src/settings-renderer/state/index.ts @@ -18,5 +18,5 @@ // of these objects. export { useGeneralSettings, useGeneralSetting } from './general-settings'; -export { useMenuSettings } from './menu-settings'; +export { useMenuSettings, useMappedMenuProperties } from './menu-settings'; export { useAppState, getSelectedChild } from './app-state'; diff --git a/src/settings-renderer/state/menu-settings.ts b/src/settings-renderer/state/menu-settings.ts index 1d9e4938f..68da5c090 100644 --- a/src/settings-renderer/state/menu-settings.ts +++ b/src/settings-renderer/state/menu-settings.ts @@ -8,6 +8,7 @@ // SPDX-FileCopyrightText: Simon Schneegans // SPDX-License-Identifier: MIT +import { useRef } from 'react'; import { create } from 'zustand'; import { produce } from 'immer'; import { temporal } from 'zundo'; @@ -160,7 +161,12 @@ type MenuStateActions = { ) => void; }; -/** Use this hook to access a slice from the settings object. */ +/** + * Use this hook to access one of the actions above or a slice which will be triggered + * whenever something changes in the collections or menus. If you are only interested in a + * subset of the properties of all menus, use the useMappedMenuProperties hook defined + * below instead. + */ export const useMenuSettings = create()( temporal( (set) => ({ @@ -379,6 +385,33 @@ export const useMenuSettings = create()( ) ); +/** + * Helper hook to memoize a mapped slice of Zustand state. This can reduce unnecessary + * re-renders when you are only interested in a subset of the properties of the menus. For + * instance, if you only want to display the names of the menus, you can use this hook to + * map the menus to their names and only re-render when the names change. + * + * @param mapFn - Mapping function to extract relevant properties from each menu. + */ +export function useMappedMenuProperties(mapFn: (menu: IMenu) => U): U[] { + const lastMenus = useRef([]); + const lastMapped = useRef([]); + return useMenuSettings((state) => { + const changed = + state.menus.length !== lastMenus.current.length || + state.menus.some((item, i) => !lodash.isEqual(mapFn(item), lastMapped.current[i])); + + if (!changed) { + return lastMapped.current; + } + + lastMenus.current = state.menus; + lastMapped.current = state.menus.map(mapFn); + + return lastMapped.current; + }); +} + // Some internal helpers ----------------------------------------------------------------- /** From 4dc186b5d7e00416e5cb7d4ce12d50da738ed3a2 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Thu, 29 May 2025 07:41:32 +0200 Subject: [PATCH 07/10] :rocket: Do not re-render entire header if undo or redo state changes --- .../components/menu-preview/PreviewHeader.tsx | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/settings-renderer/components/menu-preview/PreviewHeader.tsx b/src/settings-renderer/components/menu-preview/PreviewHeader.tsx index 8ae38e9a5..3dbf7f4d7 100644 --- a/src/settings-renderer/components/menu-preview/PreviewHeader.tsx +++ b/src/settings-renderer/components/menu-preview/PreviewHeader.tsx @@ -21,18 +21,39 @@ import { Headerbar, Button } from '../common'; * undo/redo, and for opening the settings dialogs. */ export default function PreviewHeader() { - // This will force a re-render whenever the menu settings change. For now, this is - // necessary to update the undo/redo buttons. In the future, we might want to make - // this more fine-grained, maybe by directly subscribing to the past and future states - // of the temporal state. - useMenuSettings(); - const setAboutDialogVisible = useAppState((state) => state.setAboutDialogVisible); const setIntroDialogVisible = useAppState((state) => state.setIntroDialogVisible); const setThemesDialogVisible = useAppState((state) => state.setThemesDialogVisible); const setSettingsDialogVisible = useAppState((state) => state.setSettingsDialogVisible); - const { futureStates, pastStates } = useMenuSettings.temporal.getState(); + // Undo/Redo buttons that only re-render when undo/redo state changes. + // eslint-disable-next-line @typescript-eslint/naming-convention + const UndoRedoButtons = React.memo(() => { + const { futureStates, pastStates } = useMenuSettings.temporal.getState(); + // Subscribe to any menu change to update buttons. + useMenuSettings(); + + return ( + <> +