From d634e15291d61debd0b473c8d127a4f51f35a7a0 Mon Sep 17 00:00:00 2001 From: Joey Ballentine <34788790+joeyballentine@users.noreply.github.com> Date: Tue, 21 May 2024 09:11:32 -0400 Subject: [PATCH] Add eslint-plugin-react-refresh (#2831) * Add eslint-plugin-react-refresh * Fix warnings --------- Co-authored-by: Michael Schmidt --- .eslintrc.js | 4 + package-lock.json | 10 + package.json | 1 + .../components/CustomEdge/CustomEdge.tsx | 4 +- src/renderer/components/CustomIcons.tsx | 38 -- src/renderer/components/Handle.tsx | 18 +- src/renderer/components/Header/AppInfo.tsx | 2 +- .../components/PaneNodeSearchMenu.tsx | 377 +++++++++++++++++ src/renderer/components/ReactFlowBox.tsx | 2 +- src/renderer/components/SettingsModal.tsx | 2 +- .../components/inputs/SliderInput.tsx | 16 +- .../inputs/elements/AdvanceNumberInput.tsx | 19 +- .../inputs/elements/ColorSlider.tsx | 3 +- .../inputs/elements/StyledSlider.tsx | 69 +--- .../components/node/CollapsedHandles.tsx | 4 +- src/renderer/components/node/NodeOutputs.tsx | 2 +- .../components/outputs/LargeImageOutput.tsx | 2 +- .../components/settings/components.tsx | 1 + src/renderer/contexts/AlertBoxContext.tsx | 1 + .../contexts/CollapsedNodeContext.tsx | 8 +- src/renderer/contexts/DependencyContext.tsx | 2 +- src/renderer/contexts/ExecutionContext.tsx | 4 +- src/renderer/contexts/GlobalNodeState.tsx | 2 +- src/renderer/contexts/SettingsContext.tsx | 19 +- src/renderer/helpers/colorTools.ts | 13 + src/renderer/helpers/sliderScale.ts | 68 +++ src/renderer/hooks/useIsCollapsedNode.ts | 6 + src/renderer/hooks/useLastWindowSize.ts | 2 +- src/renderer/hooks/useNodeFavorites.ts | 2 +- src/renderer/hooks/usePaneNodeSearchMenu.tsx | 387 +----------------- src/renderer/hooks/useRunNode.ts | 2 +- src/renderer/hooks/useSettings.ts | 21 + src/renderer/hooks/useThemeColor.ts | 2 +- src/renderer/hooks/useTypeColor.ts | 2 +- src/renderer/main.tsx | 2 +- 35 files changed, 546 insertions(+), 571 deletions(-) create mode 100644 src/renderer/components/PaneNodeSearchMenu.tsx create mode 100644 src/renderer/helpers/sliderScale.ts create mode 100644 src/renderer/hooks/useIsCollapsedNode.ts create mode 100644 src/renderer/hooks/useSettings.ts diff --git a/.eslintrc.js b/.eslintrc.js index dd399f786e..e65eed33a0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,7 @@ module.exports = { 'prefer-arrow-functions', 'eslint-plugin-react-memo', 'unused-imports', + 'react-refresh', ], globals: { MAIN_WINDOW_VITE_DEV_SERVER_URL: true, @@ -64,6 +65,7 @@ module.exports = { 'react/function-component-definition': 'off', 'react-memo/require-memo': ['error', { strict: true }], 'unused-imports/no-unused-imports': 'error', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], }, settings: { 'import/core-modules': ['electron'], @@ -94,6 +96,7 @@ module.exports = { 'react-hooks', 'unused-imports', 'regexp', + 'react-refresh', ], parserOptions: { project: './tsconfig.json', @@ -140,6 +143,7 @@ module.exports = { 'react-hooks/exhaustive-deps': ['warn', { additionalHooks: '(useAsyncEffect)' }], 'unused-imports/no-unused-imports': 'error', 'regexp/prefer-d': ['warn', { insideCharacterClass: 'ignore' }], + 'react-refresh/only-export-components': 'warn', }, }, { diff --git a/package-lock.json b/package-lock.json index 62fba618c5..6446727e90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,6 +107,7 @@ "eslint-plugin-react": "^7.30.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-memo": "^0.0.3", + "eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-regexp": "^2.3.0", "eslint-plugin-unused-imports": "^2.0.0", "nodemon": "^2.0.16", @@ -8412,6 +8413,15 @@ "dev": true, "license": "MIT" }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz", + "integrity": "sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "dev": true, diff --git a/package.json b/package.json index 926e29c4e5..c971a7834f 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "eslint-plugin-react": "^7.30.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-memo": "^0.0.3", + "eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-regexp": "^2.3.0", "eslint-plugin-unused-imports": "^2.0.0", "nodemon": "^2.0.16", diff --git a/src/renderer/components/CustomEdge/CustomEdge.tsx b/src/renderer/components/CustomEdge/CustomEdge.tsx index ac6e6ea9ae..770b81502b 100644 --- a/src/renderer/components/CustomEdge/CustomEdge.tsx +++ b/src/renderer/components/CustomEdge/CustomEdge.tsx @@ -10,7 +10,6 @@ import { assertNever, parseSourceHandle } from '../../../common/util'; import { BackendContext } from '../../contexts/BackendContext'; import { ExecutionContext, NodeExecutionStatus } from '../../contexts/ExecutionContext'; import { GlobalContext, GlobalVolatileContext } from '../../contexts/GlobalNodeState'; -import { useSettings } from '../../contexts/SettingsContext'; import { shadeColor } from '../../helpers/colorTools'; import { BREAKPOINT_RADIUS, @@ -19,8 +18,9 @@ import { getCustomBezierPath, } from '../../helpers/graphUtils'; import { useEdgeMenu } from '../../hooks/useEdgeMenu'; -import './CustomEdge.scss'; +import { useSettings } from '../../hooks/useSettings'; import { useTypeColor } from '../../hooks/useTypeColor'; +import './CustomEdge.scss'; const EDGE_CLASS = { RUNNING: 'running', diff --git a/src/renderer/components/CustomIcons.tsx b/src/renderer/components/CustomIcons.tsx index b2257171ca..0db24f8865 100644 --- a/src/renderer/components/CustomIcons.tsx +++ b/src/renderer/components/CustomIcons.tsx @@ -46,44 +46,6 @@ export const PyTorchIcon = createIcon({ ), }); -// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle -export const _OnnxIcon = createIcon({ - displayName: 'OnnxIcon', - viewBox: '-1.89 2.11 64 64', - path: ( - <> - - - - - - - - - ), -}); - export const OnnxIcon = createIcon({ displayName: 'OnnxIcon', viewBox: '-1.89 2.11 64 64', diff --git a/src/renderer/components/Handle.tsx b/src/renderer/components/Handle.tsx index 4bc94f641f..c6bc77de25 100644 --- a/src/renderer/components/Handle.tsx +++ b/src/renderer/components/Handle.tsx @@ -3,9 +3,10 @@ import React, { memo } from 'react'; import { Connection, Position, Handle as RFHandle } from 'reactflow'; import { useContext } from 'use-context-selector'; import { Validity } from '../../common/Validity'; -import { useIsCollapsedNode } from '../contexts/CollapsedNodeContext'; import { FakeNodeContext } from '../contexts/FakeExampleContext'; +import { createConicGradient } from '../helpers/colorTools'; import { noContextMenu } from '../hooks/useContextMenu'; +import { useIsCollapsedNode } from '../hooks/useIsCollapsedNode'; import { Markdown } from './Markdown'; export type HandleType = 'input' | 'output'; @@ -101,19 +102,6 @@ export interface HandleProps { isIterated: boolean; } -export const getBackground = (colors: readonly string[]): string => { - if (colors.length === 1) return colors[0]; - - const handleColorString = colors - .map((color, index) => { - const percent = index / colors.length; - const nextPercent = (index + 1) / colors.length; - return `${color} ${percent * 100}% ${nextPercent * 100}%`; - }) - .join(', '); - return `conic-gradient(from 90deg, ${handleColorString})`; -}; - export const Handle = memo( ({ id, @@ -161,7 +149,7 @@ export const Handle = memo( borderWidth: isConnected ? '2px' : '0px', borderColor: isConnected ? connectedColor : 'transparent', transition: '0.15s ease-in-out', - background: isConnected ? connectedBg : getBackground(handleColors), + background: isConnected ? connectedBg : createConicGradient(handleColors), boxShadow: `${type === 'input' ? '+' : '-'}2px 2px 2px #00000014`, filter: validity.isValid ? undefined : 'grayscale(100%)', opacity: validity.isValid ? 1 : 0.3, diff --git a/src/renderer/components/Header/AppInfo.tsx b/src/renderer/components/Header/AppInfo.tsx index c809e62fba..6923404b19 100644 --- a/src/renderer/components/Header/AppInfo.tsx +++ b/src/renderer/components/Header/AppInfo.tsx @@ -26,9 +26,9 @@ import { import { memo, useEffect, useRef, useState } from 'react'; import semver from 'semver'; import logo from '../../../public/icons/png/256x256.png'; -import { useSettings } from '../../contexts/SettingsContext'; import { GitHubRelease, getLatestVersionIfUpdateAvailable } from '../../helpers/github'; import { useAsyncEffect } from '../../hooks/useAsyncEffect'; +import { useSettings } from '../../hooks/useSettings'; import { useStored } from '../../hooks/useStored'; import { ipcRenderer } from '../../safeIpc'; import { Markdown } from '../Markdown'; diff --git a/src/renderer/components/PaneNodeSearchMenu.tsx b/src/renderer/components/PaneNodeSearchMenu.tsx new file mode 100644 index 0000000000..3f0dff1510 --- /dev/null +++ b/src/renderer/components/PaneNodeSearchMenu.tsx @@ -0,0 +1,377 @@ +import { CloseIcon, SearchIcon, StarIcon } from '@chakra-ui/icons'; +import { + Box, + Center, + HStack, + Icon, + Input, + InputGroup, + InputLeftElement, + InputRightElement, + MenuList, + Text, +} from '@chakra-ui/react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { VscLightbulbAutofix } from 'react-icons/vsc'; +import { CategoryMap } from '../../common/CategoryMap'; +import { Category, CategoryId, NodeSchema, SchemaId } from '../../common/common-types'; +import { assertNever, groupBy, stopPropagation } from '../../common/util'; +import { getCategoryAccentColor } from '../helpers/accentColors'; +import { interpolateColor } from '../helpers/colorTools'; +import { getMatchingNodes } from '../helpers/nodeSearchFuncs'; +import { useThemeColor } from '../hooks/useThemeColor'; +import { IconFactory } from './CustomIcons'; +import { IfVisible } from './IfVisible'; + +const clampWithWrap = (min: number, max: number, value: number): number => { + if (value < min) { + return max; + } + if (value > max) { + return min; + } + return value; +}; + +interface SchemaItemProps { + schema: NodeSchema; + isFavorite: boolean; + accentColor: string; + onClick: (schema: NodeSchema) => void; + isSelected: boolean; + scrollRef?: React.RefObject; +} +const SchemaItem = memo( + ({ schema, onClick, isFavorite, accentColor, isSelected, scrollRef }: SchemaItemProps) => { + const bgColor = useThemeColor('--bg-700'); + const menuBgColor = useThemeColor('--bg-800'); + + const gradL = interpolateColor(accentColor, menuBgColor, 0.95); + const gradR = menuBgColor; + const hoverGradL = interpolateColor(accentColor, bgColor, 0.95); + const hoverGradR = bgColor; + + return ( + onClick(schema)} + > + + + {schema.name} + + {isFavorite && ( + + )} + + ); + } +); + +type SchemaGroup = FavoritesSchemaGroup | SuggestedSchemaGroup | CategorySchemaGroup; +interface SchemaGroupBase { + readonly name: string; + readonly schemata: readonly NodeSchema[]; +} +interface FavoritesSchemaGroup extends SchemaGroupBase { + type: 'favorites'; + categoryId?: never; +} + +interface SuggestedSchemaGroup extends SchemaGroupBase { + type: 'suggested'; + categoryId?: never; +} +interface CategorySchemaGroup extends SchemaGroupBase { + type: 'category'; + categoryId: CategoryId; + category: Category | undefined; +} + +const groupSchemata = ( + schemata: readonly NodeSchema[], + categories: CategoryMap, + favorites: ReadonlySet, + suggested: ReadonlySet +): readonly SchemaGroup[] => { + const cats = [...groupBy(schemata, 'category')].map( + ([categoryId, categorySchemata]): CategorySchemaGroup => { + const category = categories.get(categoryId); + return { + type: 'category', + name: category?.name ?? categoryId, + categoryId, + category, + schemata: categorySchemata, + }; + } + ); + + const favs: FavoritesSchemaGroup = { + type: 'favorites', + name: 'Favorites', + schemata: cats.flatMap((c) => c.schemata).filter((n) => favorites.has(n.schemaId)), + }; + + const suggs: SuggestedSchemaGroup = { + type: 'suggested', + name: 'Suggested', + schemata: schemata.filter((n) => suggested.has(n.schemaId)), + }; + + return [ + ...(suggs.schemata.length ? [suggs] : []), + ...(favs.schemata.length ? [favs] : []), + ...cats, + ]; +}; + +const renderGroupIcon = (categories: CategoryMap, group: SchemaGroup) => { + switch (group.type) { + case 'favorites': + return ( + + ); + case 'suggested': + return ( + + ); + case 'category': + return ( + + ); + default: + return assertNever(group); + } +}; + +interface MenuProps { + onSelect: (schema: NodeSchema) => void; + schemata: readonly NodeSchema[]; + favorites: ReadonlySet; + categories: CategoryMap; + suggestions: ReadonlySet; +} + +export const Menu = memo( + ({ onSelect, schemata, favorites, categories, suggestions }: MenuProps) => { + const [searchQuery, setSearchQuery] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + + const changeSearchQuery = useCallback((query: string) => { + setSearchQuery(query); + setSelectedIndex(0); + }, []); + + const groups = useMemo(() => { + return groupSchemata( + getMatchingNodes(searchQuery, schemata, categories), + categories, + favorites, + suggestions + ); + }, [searchQuery, schemata, categories, favorites, suggestions]); + const flatGroups = useMemo(() => groups.flatMap((group) => group.schemata), [groups]); + + const onClickHandler = useCallback( + (schema: NodeSchema) => { + changeSearchQuery(''); + onSelect(schema); + }, + [changeSearchQuery, onSelect] + ); + const onEnterHandler = useCallback(() => { + if (selectedIndex >= 0 && selectedIndex < flatGroups.length) { + onClickHandler(flatGroups[selectedIndex]); + } + }, [flatGroups, onClickHandler, selectedIndex]); + + const keydownHandler = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + setSelectedIndex((i) => clampWithWrap(0, flatGroups.length - 1, i + 1)); + } else if (e.key === 'ArrowUp') { + setSelectedIndex((i) => clampWithWrap(0, flatGroups.length - 1, i - 1)); + } + }, + [flatGroups] + ); + useEffect(() => { + window.addEventListener('keydown', keydownHandler); + return () => window.removeEventListener('keydown', keydownHandler); + }, [keydownHandler]); + + const scrollRef = useRef(null); + useEffect(() => { + scrollRef.current?.scrollIntoView({ + block: 'center', + inline: 'nearest', + }); + }, [selectedIndex]); + + const menuBgColor = useThemeColor('--bg-800'); + const inputColor = 'var(--fg-300)'; + + return ( + + + + + + changeSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onEnterHandler(); + } + }} + /> + changeSearchQuery('')} + > + + + + + {groups.map((group, groupIndex) => { + const indexOffset = groups + .slice(0, groupIndex) + .reduce((acc, g) => acc + g.schemata.length, 0); + + const nodeHeight = 28; + const nodePadding = 2; + const placeholderHeight = + nodeHeight * group.schemata.length + + nodePadding * (group.schemata.length + 1); + + return ( + + + {renderGroupIcon(categories, group)} + {group.name} + + + + {group.schemata.map((schema, schemaIndex) => { + const index = indexOffset + schemaIndex; + const isSelected = selectedIndex === index; + + return ( + + ); + })} + + + ); + })} + + {groups.length === 0 && ( +
+ No compatible nodes found. +
+ )} +
+
+ ); + } +); diff --git a/src/renderer/components/ReactFlowBox.tsx b/src/renderer/components/ReactFlowBox.tsx index 47ec7338d8..646d4914dd 100644 --- a/src/renderer/components/ReactFlowBox.tsx +++ b/src/renderer/components/ReactFlowBox.tsx @@ -37,7 +37,6 @@ import { AlertBoxContext, AlertType } from '../contexts/AlertBoxContext'; import { BackendContext } from '../contexts/BackendContext'; import { ContextMenuContext } from '../contexts/ContextMenuContext'; import { GlobalContext, GlobalVolatileContext } from '../contexts/GlobalNodeState'; -import { useSettings } from '../contexts/SettingsContext'; import { DataTransferProcessorOptions, dataTransferProcessors } from '../helpers/dataTransfer'; import { AABB, getLayoutedPositionMap, getNodeOnEdgeIntersection } from '../helpers/graphUtils'; import { isSnappedToGrid, snapToGrid } from '../helpers/reactFlowUtil'; @@ -46,6 +45,7 @@ import { useIpcRendererListener } from '../hooks/useIpcRendererListener'; import { useMemoArray } from '../hooks/useMemo'; import { useNodesMenu } from '../hooks/useNodesMenu'; import { usePaneNodeSearchMenu } from '../hooks/usePaneNodeSearchMenu'; +import { useSettings } from '../hooks/useSettings'; import { ipcRenderer } from '../safeIpc'; const compareById = (a: Edge | Node, b: Edge | Node) => a.id.localeCompare(b.id); diff --git a/src/renderer/components/SettingsModal.tsx b/src/renderer/components/SettingsModal.tsx index 1bb293e368..acf48e8e8c 100644 --- a/src/renderer/components/SettingsModal.tsx +++ b/src/renderer/components/SettingsModal.tsx @@ -32,7 +32,7 @@ import { FaPython, FaTools } from 'react-icons/fa'; import { useContext } from 'use-context-selector'; import { log } from '../../common/log'; import { BackendContext } from '../contexts/BackendContext'; -import { useMutSetting } from '../contexts/SettingsContext'; +import { useMutSetting } from '../hooks/useSettings'; import { ipcRenderer } from '../safeIpc'; import { IconFactory } from './CustomIcons'; import { DropdownSetting, NumberSetting, ToggleSetting } from './settings/components'; diff --git a/src/renderer/components/inputs/SliderInput.tsx b/src/renderer/components/inputs/SliderInput.tsx index c21743eb30..ac5f7beebc 100644 --- a/src/renderer/components/inputs/SliderInput.tsx +++ b/src/renderer/components/inputs/SliderInput.tsx @@ -10,18 +10,11 @@ import { log } from '../../../common/log'; import { assertNever } from '../../../common/util'; import { BackendContext } from '../../contexts/BackendContext'; import { InputContext } from '../../contexts/InputContext'; +import { LINEAR_SCALE, LogScale, PowerScale, Scale } from '../../helpers/sliderScale'; import { useContextMenu } from '../../hooks/useContextMenu'; import { useInputRefactor } from '../../hooks/useInputRefactor'; import { AdvancedNumberInput } from './elements/AdvanceNumberInput'; -import { - LINEAR_SCALE, - LogScale, - PowerScale, - Scale, - SliderStyle, - StyledSlider, - getSliderHeight, -} from './elements/StyledSlider'; +import { SliderStyle, StyledSlider } from './elements/StyledSlider'; import { WithLabel, WithoutLabel } from './InputContainer'; import { InputProps } from './props'; @@ -214,12 +207,11 @@ export const SliderInput = memo( {ends[1] && {ends[1]}} { - if (n >= 1) { - // eslint-disable-next-line no-param-reassign - n %= 1; - if (areApproximatelyEqual(n, 0)) return 0; - } - if (n === 0) return 0; - return Math.min(10, n.toFixed(100).replace(/0+$/, '').split('.')[1]?.length ?? 0); -}; - interface AdvancedNumberInputProps { unit?: string | null; max: number; @@ -67,6 +56,8 @@ interface AdvancedNumberInputProps { inputWidth?: string; inputHeight?: string; noRepeatOnBlur?: boolean; + + alignSelf?: CSSProperties['alignSelf']; } export const AdvancedNumberInput = memo( @@ -90,6 +81,8 @@ export const AdvancedNumberInput = memo( inputWidth, inputHeight, noRepeatOnBlur = false, + + alignSelf, }: AdvancedNumberInputProps) => { const getNumericValue = (): number | undefined => { const rawNumber = inputString.trim() ? parseNumberString(inputString) : defaultValue; @@ -165,6 +158,7 @@ export const AdvancedNumberInput = memo( if (small) { return ( n, fromScale: (n) => n }; -export class LogScale implements Scale { - public readonly min: number; - - public readonly max: number; - - public readonly precision: number; - - public readonly offset: number; - - constructor(min: number, max: number, precision: number, offset = 0) { - this.min = min; - this.max = max; - this.precision = precision; - this.offset = offset; - } - - toScale(value: number): number { - return Math.log1p(value - (this.min + this.offset)); - } - - fromScale(scaledValue: number): number { - let value = Math.expm1(scaledValue) + (this.min + this.offset); - - // 2 digits of precision - if (this.min < value && value < this.max) { - value = Number(value.toExponential(2)); - if (value > 0) { - // we only want 1 fractional digit for numbers between 2*10^k and 10^(k+1) - const k = Math.floor(Math.log10(value)); - if (value >= 2 * 10 ** k) { - value = Number(value.toExponential(1)); - } - } - } - - return Number(value.toFixed(this.precision)); - } -} -export class PowerScale implements Scale { - public readonly power: number; - - public readonly min: number; - - public readonly precision: number; - - constructor(power: number, min: number, precision: number) { - this.power = power; - this.min = min; - this.precision = precision; - } - - toScale(value: number): number { - return (value - this.min) ** this.power; - } - - fromScale(scaledValue: number): number { - const value = scaledValue ** (1 / this.power) + this.min; - return Number(value.toFixed(this.precision)); - } -} - interface OldLabelStyle { readonly type: 'old-label'; } @@ -104,7 +39,7 @@ const getLinearGradient = (gradient: readonly string[]) => { return `linear-gradient(to right, ${gradient.join(', ')})`; }; -export const getSliderHeight = (style: SliderStyle) => { +const getSliderHeight = (style: SliderStyle) => { if (style.type === 'label') { return style.gradient ? '32px' : '28px'; } diff --git a/src/renderer/components/node/CollapsedHandles.tsx b/src/renderer/components/node/CollapsedHandles.tsx index 0d1b574748..d611b62eb7 100644 --- a/src/renderer/components/node/CollapsedHandles.tsx +++ b/src/renderer/components/node/CollapsedHandles.tsx @@ -7,10 +7,10 @@ import { Output } from '../../../common/common-types'; import { FunctionDefinition } from '../../../common/types/function'; import { stringifySourceHandle, stringifyTargetHandle } from '../../../common/util'; import { BackendContext } from '../../contexts/BackendContext'; +import { createConicGradient } from '../../helpers/colorTools'; import { NodeState } from '../../helpers/nodeState'; import { useSourceTypeColor } from '../../hooks/useSourceTypeColor'; import { useTypeColor } from '../../hooks/useTypeColor'; -import { getBackground } from '../Handle'; interface InputHandleProps { isIterated: boolean; @@ -72,7 +72,7 @@ const OutputHandle = memo( isConnectable={false} position={Position.Right} style={{ - borderColor: getBackground(handleColors), + borderColor: createConicGradient(handleColors), borderRadius: isIterated ? '10%' : '50%', }} type="source" diff --git a/src/renderer/components/node/NodeOutputs.tsx b/src/renderer/components/node/NodeOutputs.tsx index 41380fcac6..3522a379da 100644 --- a/src/renderer/components/node/NodeOutputs.tsx +++ b/src/renderer/components/node/NodeOutputs.tsx @@ -7,9 +7,9 @@ import { getChainnerScope } from '../../../common/types/chainner-scope'; import { ExpressionJson, fromJson } from '../../../common/types/json'; import { isStartingNode } from '../../../common/util'; import { BackendContext } from '../../contexts/BackendContext'; -import { useIsCollapsedNode } from '../../contexts/CollapsedNodeContext'; import { GlobalContext, GlobalVolatileContext } from '../../contexts/GlobalNodeState'; import { NodeState } from '../../helpers/nodeState'; +import { useIsCollapsedNode } from '../../hooks/useIsCollapsedNode'; import { DefaultImageOutput } from '../outputs/DefaultImageOutput'; import { GenericOutput } from '../outputs/GenericOutput'; import { LargeImageOutput } from '../outputs/LargeImageOutput'; diff --git a/src/renderer/components/outputs/LargeImageOutput.tsx b/src/renderer/components/outputs/LargeImageOutput.tsx index fc1b87cd23..6b1f7f789c 100644 --- a/src/renderer/components/outputs/LargeImageOutput.tsx +++ b/src/renderer/components/outputs/LargeImageOutput.tsx @@ -7,9 +7,9 @@ import { useTranslation } from 'react-i18next'; import { useContextSelector } from 'use-context-selector'; import { Size } from '../../../common/common-types'; import { GlobalVolatileContext } from '../../contexts/GlobalNodeState'; -import { useSettings } from '../../contexts/SettingsContext'; import { useDevicePixelRatio } from '../../hooks/useDevicePixelRatio'; import { useMemoArray } from '../../hooks/useMemo'; +import { useSettings } from '../../hooks/useSettings'; import { DragHandleSVG } from '../CustomIcons'; import { OutputProps } from './props'; diff --git a/src/renderer/components/settings/components.tsx b/src/renderer/components/settings/components.tsx index a197b7dc9d..98f7d04b40 100644 --- a/src/renderer/components/settings/components.tsx +++ b/src/renderer/components/settings/components.tsx @@ -174,6 +174,7 @@ const CacheSetting = memo(({ setting, value, setValue }: SettingsProps<'cache'>) ); }); +// eslint-disable-next-line react-refresh/only-export-components export const SettingComponents: { readonly [K in Setting['type']]: React.MemoExoticComponent< (props: SettingsProps) => JSX.Element diff --git a/src/renderer/contexts/AlertBoxContext.tsx b/src/renderer/contexts/AlertBoxContext.tsx index ed751253cd..809e24f042 100644 --- a/src/renderer/contexts/AlertBoxContext.tsx +++ b/src/renderer/contexts/AlertBoxContext.tsx @@ -35,6 +35,7 @@ interface AlertBox { showAlert: (message: AlertOptions) => Promise; } +// eslint-disable-next-line react-refresh/only-export-components export enum AlertType { INFO = 'Info', WARN = 'Warning', diff --git a/src/renderer/contexts/CollapsedNodeContext.tsx b/src/renderer/contexts/CollapsedNodeContext.tsx index 87465dea6e..387e4a34ed 100644 --- a/src/renderer/contexts/CollapsedNodeContext.tsx +++ b/src/renderer/contexts/CollapsedNodeContext.tsx @@ -1,8 +1,8 @@ import { Box } from '@chakra-ui/react'; import React, { memo } from 'react'; -import { createContext, useContext } from 'use-context-selector'; +import { createContext } from 'use-context-selector'; -const IsCollapsedContext = createContext(false); +export const IsCollapsedContext = createContext(false); export const CollapsedNode = memo(({ children }: React.PropsWithChildren) => { return ( @@ -14,7 +14,3 @@ export const CollapsedNode = memo(({ children }: React.PropsWithChildren ); }); - -export const useIsCollapsedNode = (): boolean => { - return useContext(IsCollapsedContext); -}; diff --git a/src/renderer/contexts/DependencyContext.tsx b/src/renderer/contexts/DependencyContext.tsx index 7062b9c738..71cfb5a75b 100644 --- a/src/renderer/contexts/DependencyContext.tsx +++ b/src/renderer/contexts/DependencyContext.tsx @@ -57,10 +57,10 @@ import { useBackendSetupEventSource, } from '../hooks/useBackendEventSource'; import { useMemoObject } from '../hooks/useMemo'; +import { useSettings } from '../hooks/useSettings'; import { AlertBoxContext, AlertType } from './AlertBoxContext'; import { BackendContext } from './BackendContext'; import { GlobalContext } from './GlobalNodeState'; -import { useSettings } from './SettingsContext'; export interface DependencyContextValue { openDependencyManager: () => void; diff --git a/src/renderer/contexts/ExecutionContext.tsx b/src/renderer/contexts/ExecutionContext.tsx index 406339b45c..bed11ea23c 100644 --- a/src/renderer/contexts/ExecutionContext.tsx +++ b/src/renderer/contexts/ExecutionContext.tsx @@ -28,12 +28,13 @@ import { } from '../hooks/useBackendEventSource'; import { EventBacklog, useEventBacklog } from '../hooks/useEventBacklog'; import { useMemoObject } from '../hooks/useMemo'; +import { useSettings } from '../hooks/useSettings'; import { ipcRenderer } from '../safeIpc'; import { AlertBoxContext, AlertType } from './AlertBoxContext'; import { BackendContext } from './BackendContext'; import { GlobalContext, GlobalVolatileContext } from './GlobalNodeState'; -import { useSettings } from './SettingsContext'; +// eslint-disable-next-line react-refresh/only-export-components export enum ExecutionStatus { READY, RUNNING, @@ -41,6 +42,7 @@ export enum ExecutionStatus { KILLING, } +// eslint-disable-next-line react-refresh/only-export-components export enum NodeExecutionStatus { /** * The node has not been run yet and is awaiting execution. diff --git a/src/renderer/contexts/GlobalNodeState.tsx b/src/renderer/contexts/GlobalNodeState.tsx index 029ce0a1c4..6a538e587d 100644 --- a/src/renderer/contexts/GlobalNodeState.tsx +++ b/src/renderer/contexts/GlobalNodeState.tsx @@ -82,10 +82,10 @@ import { useOutputDataStore, } from '../hooks/useOutputDataStore'; import { getSessionStorageOrDefault, useSessionStorage } from '../hooks/useSessionStorage'; +import { useSettings } from '../hooks/useSettings'; import { ipcRenderer } from '../safeIpc'; import { AlertBoxContext, AlertType } from './AlertBoxContext'; import { BackendContext } from './BackendContext'; -import { useSettings } from './SettingsContext'; import type { ParsedSaveData, SaveData } from '../../main/SaveFile'; const EMPTY_CONNECTED: readonly [IdSet, IdSet] = [IdSet.empty, IdSet.empty]; diff --git a/src/renderer/contexts/SettingsContext.tsx b/src/renderer/contexts/SettingsContext.tsx index 0c20e8554a..119d999429 100644 --- a/src/renderer/contexts/SettingsContext.tsx +++ b/src/renderer/contexts/SettingsContext.tsx @@ -1,6 +1,6 @@ import { useColorMode } from '@chakra-ui/react'; import React, { SetStateAction, memo, useCallback, useEffect, useState } from 'react'; -import { createContext, useContext } from 'use-context-selector'; +import { createContext } from 'use-context-selector'; import { log } from '../../common/log'; import { ChainnerSettings, defaultSettings } from '../../common/settings/settings'; import { noop } from '../../common/util'; @@ -80,20 +80,3 @@ export const SettingsProvider = memo( return {children}; } ); - -export const useSettings = () => { - return useContext(SettingsContext).settings; -}; - -export const useMutSetting = (key: K) => { - const { settings, setSetting } = useContext(SettingsContext); - - const set = useCallback( - (update: SetStateAction) => { - setSetting(key, update); - }, - [key, setSetting] - ); - - return [settings[key], set] as const; -}; diff --git a/src/renderer/helpers/colorTools.ts b/src/renderer/helpers/colorTools.ts index dcf5d95393..1990b26ed6 100644 --- a/src/renderer/helpers/colorTools.ts +++ b/src/renderer/helpers/colorTools.ts @@ -57,3 +57,16 @@ const interpolateColorImpl = ( export const interpolateColor = (color1: string, color2: string, factor = 0.5): string => rgbToHex(interpolateColorImpl(hexToRgb(color1), hexToRgb(color2), factor)); + +export const createConicGradient = (colors: readonly string[]): string => { + if (colors.length === 1) return colors[0]; + + const handleColorString = colors + .map((color, index) => { + const percent = index / colors.length; + const nextPercent = (index + 1) / colors.length; + return `${color} ${percent * 100}% ${nextPercent * 100}%`; + }) + .join(', '); + return `conic-gradient(from 90deg, ${handleColorString})`; +}; diff --git a/src/renderer/helpers/sliderScale.ts b/src/renderer/helpers/sliderScale.ts new file mode 100644 index 0000000000..8866309a93 --- /dev/null +++ b/src/renderer/helpers/sliderScale.ts @@ -0,0 +1,68 @@ +export interface Scale { + toScale(value: number): number; + fromScale(scaledValue: number): number; +} + +export const LINEAR_SCALE: Scale = { toScale: (n) => n, fromScale: (n) => n }; + +export class LogScale implements Scale { + public readonly min: number; + + public readonly max: number; + + public readonly precision: number; + + public readonly offset: number; + + constructor(min: number, max: number, precision: number, offset = 0) { + this.min = min; + this.max = max; + this.precision = precision; + this.offset = offset; + } + + toScale(value: number): number { + return Math.log1p(value - (this.min + this.offset)); + } + + fromScale(scaledValue: number): number { + let value = Math.expm1(scaledValue) + (this.min + this.offset); + + // 2 digits of precision + if (this.min < value && value < this.max) { + value = Number(value.toExponential(2)); + if (value > 0) { + // we only want 1 fractional digit for numbers between 2*10^k and 10^(k+1) + const k = Math.floor(Math.log10(value)); + if (value >= 2 * 10 ** k) { + value = Number(value.toExponential(1)); + } + } + } + + return Number(value.toFixed(this.precision)); + } +} + +export class PowerScale implements Scale { + public readonly power: number; + + public readonly min: number; + + public readonly precision: number; + + constructor(power: number, min: number, precision: number) { + this.power = power; + this.min = min; + this.precision = precision; + } + + toScale(value: number): number { + return (value - this.min) ** this.power; + } + + fromScale(scaledValue: number): number { + const value = scaledValue ** (1 / this.power) + this.min; + return Number(value.toFixed(this.precision)); + } +} diff --git a/src/renderer/hooks/useIsCollapsedNode.ts b/src/renderer/hooks/useIsCollapsedNode.ts new file mode 100644 index 0000000000..59956d6eff --- /dev/null +++ b/src/renderer/hooks/useIsCollapsedNode.ts @@ -0,0 +1,6 @@ +import { useContext } from 'use-context-selector'; +import { IsCollapsedContext } from '../contexts/CollapsedNodeContext'; + +export const useIsCollapsedNode = (): boolean => { + return useContext(IsCollapsedContext); +}; diff --git a/src/renderer/hooks/useLastWindowSize.ts b/src/renderer/hooks/useLastWindowSize.ts index b52928529c..878efb8554 100644 --- a/src/renderer/hooks/useLastWindowSize.ts +++ b/src/renderer/hooks/useLastWindowSize.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react'; import { debounce } from '../../common/util'; -import { useMutSetting } from '../contexts/SettingsContext'; import { useIpcRendererListener } from './useIpcRendererListener'; +import { useMutSetting } from './useSettings'; export const useLastWindowSize = () => { const [, setSize] = useMutSetting('lastWindowSize'); diff --git a/src/renderer/hooks/useNodeFavorites.ts b/src/renderer/hooks/useNodeFavorites.ts index e43eb4fa8f..6bb8fd0b17 100644 --- a/src/renderer/hooks/useNodeFavorites.ts +++ b/src/renderer/hooks/useNodeFavorites.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo } from 'react'; import { SchemaId } from '../../common/common-types'; -import { useMutSetting } from '../contexts/SettingsContext'; import { useMemoObject } from './useMemo'; +import { useMutSetting } from './useSettings'; export interface UseNodeFavorites { readonly favorites: ReadonlySet; diff --git a/src/renderer/hooks/usePaneNodeSearchMenu.tsx b/src/renderer/hooks/usePaneNodeSearchMenu.tsx index c343daefdd..7f46f11439 100644 --- a/src/renderer/hooks/usePaneNodeSearchMenu.tsx +++ b/src/renderer/hooks/usePaneNodeSearchMenu.tsx @@ -1,29 +1,7 @@ -import { CloseIcon, SearchIcon, StarIcon } from '@chakra-ui/icons'; -import { - Box, - Center, - HStack, - Icon, - Input, - InputGroup, - InputLeftElement, - InputRightElement, - MenuList, - Text, -} from '@chakra-ui/react'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { VscLightbulbAutofix } from 'react-icons/vsc'; +import { useCallback, useMemo, useState } from 'react'; import { OnConnectStartParams, useReactFlow } from 'reactflow'; import { useContext, useContextSelector } from 'use-context-selector'; -import { CategoryMap } from '../../common/CategoryMap'; -import { - Category, - CategoryId, - InputId, - NodeSchema, - OutputId, - SchemaId, -} from '../../common/common-types'; +import { InputId, NodeSchema, OutputId, SchemaId } from '../../common/common-types'; import { getFirstPossibleInput, getFirstPossibleOutput } from '../../common/nodes/connectedInputs'; import { ChainLineage } from '../../common/nodes/lineage'; import { TypeState } from '../../common/nodes/TypeState'; @@ -32,377 +10,18 @@ import { EMPTY_SET, assertNever, createUniqueId, - groupBy, isNotNullish, parseSourceHandle, parseTargetHandle, - stopPropagation, stringifySourceHandle, stringifyTargetHandle, } from '../../common/util'; -import { IconFactory } from '../components/CustomIcons'; -import { IfVisible } from '../components/IfVisible'; +import { Menu } from '../components/PaneNodeSearchMenu'; import { BackendContext } from '../contexts/BackendContext'; import { ContextMenuContext } from '../contexts/ContextMenuContext'; import { GlobalContext, GlobalVolatileContext } from '../contexts/GlobalNodeState'; -import { getCategoryAccentColor } from '../helpers/accentColors'; -import { interpolateColor } from '../helpers/colorTools'; - -import { getMatchingNodes } from '../helpers/nodeSearchFuncs'; import { useContextMenu } from './useContextMenu'; import { useNodeFavorites } from './useNodeFavorites'; -import { useThemeColor } from './useThemeColor'; - -const clampWithWrap = (min: number, max: number, value: number): number => { - if (value < min) { - return max; - } - if (value > max) { - return min; - } - return value; -}; - -interface SchemaItemProps { - schema: NodeSchema; - isFavorite: boolean; - accentColor: string; - onClick: (schema: NodeSchema) => void; - isSelected: boolean; - scrollRef?: React.RefObject; -} -const SchemaItem = memo( - ({ schema, onClick, isFavorite, accentColor, isSelected, scrollRef }: SchemaItemProps) => { - const bgColor = useThemeColor('--bg-700'); - const menuBgColor = useThemeColor('--bg-800'); - - const gradL = interpolateColor(accentColor, menuBgColor, 0.95); - const gradR = menuBgColor; - const hoverGradL = interpolateColor(accentColor, bgColor, 0.95); - const hoverGradR = bgColor; - - return ( - onClick(schema)} - > - - - {schema.name} - - {isFavorite && ( - - )} - - ); - } -); - -type SchemaGroup = FavoritesSchemaGroup | SuggestedSchemaGroup | CategorySchemaGroup; -interface SchemaGroupBase { - readonly name: string; - readonly schemata: readonly NodeSchema[]; -} -interface FavoritesSchemaGroup extends SchemaGroupBase { - type: 'favorites'; - categoryId?: never; -} - -interface SuggestedSchemaGroup extends SchemaGroupBase { - type: 'suggested'; - categoryId?: never; -} -interface CategorySchemaGroup extends SchemaGroupBase { - type: 'category'; - categoryId: CategoryId; - category: Category | undefined; -} - -const groupSchemata = ( - schemata: readonly NodeSchema[], - categories: CategoryMap, - favorites: ReadonlySet, - suggested: ReadonlySet -): readonly SchemaGroup[] => { - const cats = [...groupBy(schemata, 'category')].map( - ([categoryId, categorySchemata]): CategorySchemaGroup => { - const category = categories.get(categoryId); - return { - type: 'category', - name: category?.name ?? categoryId, - categoryId, - category, - schemata: categorySchemata, - }; - } - ); - - const favs: FavoritesSchemaGroup = { - type: 'favorites', - name: 'Favorites', - schemata: cats.flatMap((c) => c.schemata).filter((n) => favorites.has(n.schemaId)), - }; - - const suggs: SuggestedSchemaGroup = { - type: 'suggested', - name: 'Suggested', - schemata: schemata.filter((n) => suggested.has(n.schemaId)), - }; - - return [ - ...(suggs.schemata.length ? [suggs] : []), - ...(favs.schemata.length ? [favs] : []), - ...cats, - ]; -}; - -const renderGroupIcon = (categories: CategoryMap, group: SchemaGroup) => { - switch (group.type) { - case 'favorites': - return ( - - ); - case 'suggested': - return ( - - ); - case 'category': - return ( - - ); - default: - return assertNever(group); - } -}; - -interface MenuProps { - onSelect: (schema: NodeSchema) => void; - schemata: readonly NodeSchema[]; - favorites: ReadonlySet; - categories: CategoryMap; - suggestions: ReadonlySet; -} - -const Menu = memo(({ onSelect, schemata, favorites, categories, suggestions }: MenuProps) => { - const [searchQuery, setSearchQuery] = useState(''); - const [selectedIndex, setSelectedIndex] = useState(0); - - const changeSearchQuery = useCallback((query: string) => { - setSearchQuery(query); - setSelectedIndex(0); - }, []); - - const groups = useMemo(() => { - return groupSchemata( - getMatchingNodes(searchQuery, schemata, categories), - categories, - favorites, - suggestions - ); - }, [searchQuery, schemata, categories, favorites, suggestions]); - const flatGroups = useMemo(() => groups.flatMap((group) => group.schemata), [groups]); - - const onClickHandler = useCallback( - (schema: NodeSchema) => { - changeSearchQuery(''); - onSelect(schema); - }, - [changeSearchQuery, onSelect] - ); - const onEnterHandler = useCallback(() => { - if (selectedIndex >= 0 && selectedIndex < flatGroups.length) { - onClickHandler(flatGroups[selectedIndex]); - } - }, [flatGroups, onClickHandler, selectedIndex]); - - const keydownHandler = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'ArrowDown') { - setSelectedIndex((i) => clampWithWrap(0, flatGroups.length - 1, i + 1)); - } else if (e.key === 'ArrowUp') { - setSelectedIndex((i) => clampWithWrap(0, flatGroups.length - 1, i - 1)); - } - }, - [flatGroups] - ); - useEffect(() => { - window.addEventListener('keydown', keydownHandler); - return () => window.removeEventListener('keydown', keydownHandler); - }, [keydownHandler]); - - const scrollRef = useRef(null); - useEffect(() => { - scrollRef.current?.scrollIntoView({ - block: 'center', - inline: 'nearest', - }); - }, [selectedIndex]); - - const menuBgColor = useThemeColor('--bg-800'); - const inputColor = 'var(--fg-300)'; - - return ( - - - - - - changeSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onEnterHandler(); - } - }} - /> - changeSearchQuery('')} - > - - - - - {groups.map((group, groupIndex) => { - const indexOffset = groups - .slice(0, groupIndex) - .reduce((acc, g) => acc + g.schemata.length, 0); - - const nodeHeight = 28; - const nodePadding = 2; - const placeholderHeight = - nodeHeight * group.schemata.length + - nodePadding * (group.schemata.length + 1); - - return ( - - - {renderGroupIcon(categories, group)} - {group.name} - - - - {group.schemata.map((schema, schemaIndex) => { - const index = indexOffset + schemaIndex; - const isSelected = selectedIndex === index; - - return ( - - ); - })} - - - ); - })} - - {groups.length === 0 && ( -
- No compatible nodes found. -
- )} -
-
- ); -}); type ConnectionStart = ConnectionStartSource | ConnectionStartTarget; type ConnectionStartSource = { type: 'source'; nodeId: string; outputId: OutputId }; diff --git a/src/renderer/hooks/useRunNode.ts b/src/renderer/hooks/useRunNode.ts index 145ab173cd..9589aed524 100644 --- a/src/renderer/hooks/useRunNode.ts +++ b/src/renderer/hooks/useRunNode.ts @@ -6,8 +6,8 @@ import { delay, mapInputValues } from '../../common/util'; import { AlertBoxContext } from '../contexts/AlertBoxContext'; import { BackendContext } from '../contexts/BackendContext'; import { GlobalContext } from '../contexts/GlobalNodeState'; -import { useSettings } from '../contexts/SettingsContext'; import { useAsyncEffect } from './useAsyncEffect'; +import { useSettings } from './useSettings'; /** * Runs the given node as soon as it should. diff --git a/src/renderer/hooks/useSettings.ts b/src/renderer/hooks/useSettings.ts new file mode 100644 index 0000000000..5ba93d402b --- /dev/null +++ b/src/renderer/hooks/useSettings.ts @@ -0,0 +1,21 @@ +import { SetStateAction, useCallback } from 'react'; +import { useContext } from 'use-context-selector'; +import { ChainnerSettings } from '../../common/settings/settings'; +import { SettingsContext } from '../contexts/SettingsContext'; + +export const useSettings = () => { + return useContext(SettingsContext).settings; +}; + +export const useMutSetting = (key: K) => { + const { settings, setSetting } = useContext(SettingsContext); + + const set = useCallback( + (update: SetStateAction) => { + setSetting(key, update); + }, + [key, setSetting] + ); + + return [settings[key], set] as const; +}; diff --git a/src/renderer/hooks/useThemeColor.ts b/src/renderer/hooks/useThemeColor.ts index 1a2dc8754f..b6c57337c6 100644 --- a/src/renderer/hooks/useThemeColor.ts +++ b/src/renderer/hooks/useThemeColor.ts @@ -1,7 +1,7 @@ import { useColorMode } from '@chakra-ui/react'; import { useMemo } from 'react'; import { lazy } from '../../common/util'; -import { useSettings } from '../contexts/SettingsContext'; +import { useSettings } from './useSettings'; const light = lazy(() => getComputedStyle(document.documentElement)); const dark = lazy(() => getComputedStyle(document.documentElement)); diff --git a/src/renderer/hooks/useTypeColor.ts b/src/renderer/hooks/useTypeColor.ts index e1f9c73121..f5789d3dcb 100644 --- a/src/renderer/hooks/useTypeColor.ts +++ b/src/renderer/hooks/useTypeColor.ts @@ -1,7 +1,7 @@ import { Type } from '@chainner/navi'; import { useMemo } from 'react'; -import { useSettings } from '../contexts/SettingsContext'; import { defaultColor, getTypeAccentColors } from '../helpers/accentColors'; +import { useSettings } from './useSettings'; export const useTypeColor = (type: Type | undefined) => { const { theme } = useSettings(); diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index c295d1aba4..ddca8286e3 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -18,9 +18,9 @@ import { DependencyProvider } from './contexts/DependencyContext'; import { ExecutionProvider } from './contexts/ExecutionContext'; import { GlobalProvider } from './contexts/GlobalNodeState'; import { NodeDocumentationProvider } from './contexts/NodeDocumentationContext'; -import { useSettings } from './contexts/SettingsContext'; import { useIpcRendererListener } from './hooks/useIpcRendererListener'; import { useLastWindowSize } from './hooks/useLastWindowSize'; +import { useSettings } from './hooks/useSettings'; import { ipcRenderer } from './safeIpc'; const nodeTypes: NodeTypes & Record = {