From f5617aca1c4c95a38cc439da9230f168e604e724 Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Sun, 2 Jul 2023 17:41:02 +0100
Subject: [PATCH] :recycle:  moving more components out of common.tsx (#1257)

Moving some more components out of `common.tsx` into their own files.

There are no functional changes. This is a direct copy&paste into new
files.
---
 .../desktop-client/src/components/common.tsx  | 434 +-----------------
 .../src/components/common/AlignedText.tsx     |  56 +++
 .../src/components/common/CustomSelect.tsx    |  52 +++
 .../src/components/common/FormError.tsx       |  16 +
 .../src/components/common/InitialFocus.tsx    |  34 ++
 .../components/common/InputWithContent.tsx    |  72 +++
 .../src/components/common/Menu.tsx            | 181 ++++++++
 .../src/components/common/Search.tsx          |  43 ++
 upcoming-release-notes/1257.md                |   6 +
 9 files changed, 471 insertions(+), 423 deletions(-)
 create mode 100644 packages/desktop-client/src/components/common/AlignedText.tsx
 create mode 100644 packages/desktop-client/src/components/common/CustomSelect.tsx
 create mode 100644 packages/desktop-client/src/components/common/FormError.tsx
 create mode 100644 packages/desktop-client/src/components/common/InitialFocus.tsx
 create mode 100644 packages/desktop-client/src/components/common/InputWithContent.tsx
 create mode 100644 packages/desktop-client/src/components/common/Menu.tsx
 create mode 100644 packages/desktop-client/src/components/common/Search.tsx
 create mode 100644 upcoming-release-notes/1257.md

diff --git a/packages/desktop-client/src/components/common.tsx b/packages/desktop-client/src/components/common.tsx
index 554b5c6bb..d63d82331 100644
--- a/packages/desktop-client/src/components/common.tsx
+++ b/packages/desktop-client/src/components/common.tsx
@@ -1,52 +1,40 @@
 import React, {
   useRef,
-  useEffect,
   useLayoutEffect,
-  useState,
   useCallback,
-  type ChangeEvent,
   type ComponentProps,
-  type ReactElement,
   type ReactNode,
-  type Ref,
   forwardRef,
-  createElement,
-  cloneElement,
 } from 'react';
 import { NavLink, useMatch, useNavigate } from 'react-router-dom';
 
-import {
-  ListboxInput,
-  ListboxButton,
-  ListboxPopover,
-  ListboxList,
-  ListboxOption,
-} from '@reach/listbox';
 import { type CSSProperties, css } from 'glamor';
 
-import ExpandArrow from '../icons/v0/ExpandArrow';
 import { styles, colors } from '../style';
 import type { HTMLPropsWithStyle } from '../types/utils';
 
-import Block from './common/Block';
 import Button from './common/Button';
-import Input, { defaultInputStyle } from './common/Input';
-import Text from './common/Text';
-import View from './common/View';
 
-export { default as Modal, ModalButtons } from './common/Modal';
+export { default as AlignedText } from './common/AlignedText';
 export { default as Block } from './common/Block';
 export { default as Button, ButtonWithLoading } from './common/Button';
 export { default as Card } from './common/Card';
+export { default as CustomSelect } from './common/CustomSelect';
+export { default as FormError } from './common/FormError';
 export { default as HoverTarget } from './common/HoverTarget';
+export { default as InitialFocus } from './common/InitialFocus';
 export { default as InlineField } from './common/InlineField';
 export { default as Input } from './common/Input';
+export { default as InputWithContent } from './common/InputWithContent';
 export { default as Label } from './common/Label';
-export { default as View } from './common/View';
-export { default as Text } from './common/Text';
-export { default as TextOneLine } from './common/TextOneLine';
+export { default as Menu } from './common/Menu';
+export { default as Modal, ModalButtons } from './common/Modal';
+export { default as Search } from './common/Search';
 export { default as Select } from './common/Select';
 export { default as Stack } from './Stack';
+export { default as Text } from './common/Text';
+export { default as TextOneLine } from './common/TextOneLine';
+export { default as View } from './common/View';
 
 type UseStableCallbackArg = (...args: unknown[]) => unknown;
 
@@ -174,368 +162,6 @@ export function ButtonLink({
   );
 }
 
-type InputWithContentProps = ComponentProps<typeof Input> & {
-  leftContent: ReactNode;
-  rightContent: ReactNode;
-  inputStyle?: CSSProperties;
-  style?: CSSProperties;
-  getStyle?: (focused: boolean) => CSSProperties;
-};
-export function InputWithContent({
-  leftContent,
-  rightContent,
-  inputStyle,
-  style,
-  getStyle,
-  ...props
-}: InputWithContentProps) {
-  let [focused, setFocused] = useState(false);
-
-  return (
-    <View
-      style={[
-        defaultInputStyle,
-        {
-          padding: 0,
-          flex: 1,
-          flexDirection: 'row',
-          alignItems: 'center',
-        },
-        focused && {
-          border: '1px solid ' + colors.b5,
-          boxShadow: '0 1px 1px ' + colors.b7,
-        },
-        style,
-        getStyle && getStyle(focused),
-      ]}
-    >
-      {leftContent}
-      <Input
-        {...props}
-        style={[
-          inputStyle,
-          {
-            flex: 1,
-            '&, &:focus, &:hover': {
-              border: 0,
-              backgroundColor: 'transparent',
-              boxShadow: 'none',
-              color: 'inherit',
-            },
-          },
-        ]}
-        onFocus={e => {
-          setFocused(true);
-          props.onFocus && props.onFocus(e);
-        }}
-        onBlur={e => {
-          setFocused(false);
-          props.onBlur && props.onBlur(e);
-        }}
-      />
-      {rightContent}
-    </View>
-  );
-}
-
-type SearchProps = {
-  inputRef: Ref<HTMLInputElement>;
-  value: string;
-  onChange: (value: string) => unknown;
-  placeholder: string;
-  isInModal: boolean;
-  width?: number;
-};
-export function Search({
-  inputRef,
-  value,
-  onChange,
-  placeholder,
-  isInModal,
-  width = 350,
-}: SearchProps) {
-  return (
-    <Input
-      inputRef={inputRef}
-      placeholder={placeholder}
-      value={value}
-      onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
-      style={{
-        width,
-        borderColor: isInModal ? null : 'transparent',
-        backgroundColor: isInModal ? null : colors.n11,
-        ':focus': isInModal
-          ? null
-          : {
-              backgroundColor: 'white',
-              '::placeholder': { color: colors.n8 },
-            },
-      }}
-    />
-  );
-}
-
-type CustomSelectProps = {
-  options: Array<[string, string]>;
-  value: string;
-  onChange?: (newValue: string) => void;
-  style?: CSSProperties;
-  disabledKeys?: string[];
-};
-
-export function CustomSelect({
-  options,
-  value,
-  onChange,
-  style,
-  disabledKeys = [],
-}: CustomSelectProps) {
-  return (
-    <ListboxInput
-      value={value}
-      onChange={onChange}
-      style={{ lineHeight: '1em' }}
-    >
-      <ListboxButton
-        {...css([{ borderWidth: 0, padding: 5, borderRadius: 4 }, style])}
-        arrow={<ExpandArrow style={{ width: 7, height: 7, paddingTop: 3 }} />}
-      />
-      <ListboxPopover style={{ zIndex: 10000, outline: 0, borderRadius: 4 }}>
-        <ListboxList>
-          {options.map(([value, label]) => (
-            <ListboxOption
-              key={value}
-              value={value}
-              disabled={disabledKeys.includes(value)}
-            >
-              {label}
-            </ListboxOption>
-          ))}
-        </ListboxList>
-      </ListboxPopover>
-    </ListboxInput>
-  );
-}
-
-type KeybindingProps = {
-  keyName: ReactNode;
-};
-
-function Keybinding({ keyName }: KeybindingProps) {
-  return <Text style={{ fontSize: 10, color: colors.n6 }}>{keyName}</Text>;
-}
-
-type MenuItem = {
-  type?: string | symbol;
-  name: string;
-  disabled?: boolean;
-  icon?;
-  iconSize?: number;
-  text: string;
-  key?: string;
-};
-
-type MenuProps = {
-  header?: ReactNode;
-  footer?: ReactNode;
-  items: Array<MenuItem | typeof Menu.line>;
-  onMenuSelect;
-};
-
-export function Menu({
-  header,
-  footer,
-  items: allItems,
-  onMenuSelect,
-}: MenuProps) {
-  let elRef = useRef(null);
-  let items = allItems.filter(x => x);
-  let [hoveredIndex, setHoveredIndex] = useState(null);
-
-  useEffect(() => {
-    const el = elRef.current;
-    el.focus();
-
-    let onKeyDown = e => {
-      let filteredItems = items.filter(
-        item => item && item !== Menu.line && item.type !== Menu.label,
-      );
-      let currentIndex = filteredItems.indexOf(items[hoveredIndex]);
-
-      let transformIndex = idx => items.indexOf(filteredItems[idx]);
-
-      switch (e.key) {
-        case 'ArrowUp':
-          e.preventDefault();
-          setHoveredIndex(
-            hoveredIndex === null
-              ? 0
-              : transformIndex(Math.max(currentIndex - 1, 0)),
-          );
-          break;
-        case 'ArrowDown':
-          e.preventDefault();
-          setHoveredIndex(
-            hoveredIndex === null
-              ? 0
-              : transformIndex(
-                  Math.min(currentIndex + 1, filteredItems.length - 1),
-                ),
-          );
-          break;
-        case 'Enter':
-          e.preventDefault();
-          const item = items[hoveredIndex];
-          if (hoveredIndex !== null && item !== Menu.line) {
-            onMenuSelect && onMenuSelect(item.name);
-          }
-          break;
-        default:
-      }
-    };
-
-    el.addEventListener('keydown', onKeyDown);
-
-    return () => {
-      el.removeEventListener('keydown', onKeyDown);
-    };
-  }, [hoveredIndex]);
-
-  return (
-    <View
-      style={{ outline: 'none', borderRadius: 4, overflow: 'hidden' }}
-      tabIndex={1}
-      innerRef={elRef}
-    >
-      {header}
-      {items.map((item, idx) => {
-        if (item === Menu.line) {
-          return (
-            <View key={idx} style={{ margin: '3px 0px' }}>
-              <View style={{ borderTop: '1px solid ' + colors.n10 }} />
-            </View>
-          );
-        } else if (item.type === Menu.label) {
-          return (
-            <Text
-              key={item.name}
-              style={{
-                color: colors.n6,
-                fontSize: 11,
-                lineHeight: '1em',
-                textTransform: 'uppercase',
-                margin: '3px 9px',
-                marginTop: 5,
-              }}
-            >
-              {item.name}
-            </Text>
-          );
-        }
-
-        let lastItem = items[idx - 1];
-
-        return (
-          <View
-            role="button"
-            key={item.name}
-            style={[
-              {
-                cursor: 'default',
-                padding: '9px 10px',
-                marginTop:
-                  idx === 0 ||
-                  lastItem === Menu.line ||
-                  lastItem.type === Menu.label
-                    ? 0
-                    : -3,
-                flexDirection: 'row',
-                alignItems: 'center',
-              },
-              item.disabled && { color: colors.n7 },
-              !item.disabled &&
-                hoveredIndex === idx && { backgroundColor: colors.n10 },
-            ]}
-            onMouseEnter={() => setHoveredIndex(idx)}
-            onMouseLeave={() => setHoveredIndex(null)}
-            onClick={e =>
-              !item.disabled && onMenuSelect && onMenuSelect(item.name)
-            }
-          >
-            {/* Force it to line up evenly */}
-            <Text style={{ lineHeight: 0 }}>
-              {item.icon &&
-                createElement(item.icon, {
-                  width: item.iconSize || 10,
-                  height: item.iconSize || 10,
-                  style: { marginRight: 7, width: 10 },
-                })}
-            </Text>
-            <Text>{item.text}</Text>
-            <View style={{ flex: 1 }} />
-            {item.key && <Keybinding keyName={item.key} />}
-          </View>
-        );
-      })}
-      {footer}
-    </View>
-  );
-}
-
-const MenuLine: unique symbol = Symbol('menu-line');
-Menu.line = MenuLine;
-Menu.label = Symbol('menu-label');
-
-type AlignedTextProps = ComponentProps<typeof View> & {
-  left;
-  right;
-  style?: CSSProperties;
-  leftStyle?: CSSProperties;
-  rightStyle?: CSSProperties;
-  truncate?: 'left' | 'right';
-};
-export function AlignedText({
-  left,
-  right,
-  style,
-  leftStyle,
-  rightStyle,
-  truncate = 'left',
-  ...nativeProps
-}: AlignedTextProps) {
-  const truncateStyle = {
-    textOverflow: 'ellipsis',
-    whiteSpace: 'nowrap',
-    overflow: 'hidden',
-  };
-
-  return (
-    <View
-      style={[{ flexDirection: 'row', alignItems: 'center' }, style]}
-      {...nativeProps}
-    >
-      <Block
-        style={[
-          { marginRight: 10 },
-          truncate === 'left' && truncateStyle,
-          leftStyle,
-        ]}
-      >
-        {left}
-      </Block>
-      <Block
-        style={[
-          { flex: 1, textAlign: 'right' },
-          truncate === 'right' && truncateStyle,
-          rightStyle,
-        ]}
-      >
-        {right}
-      </Block>
-    </View>
-  );
-}
-
 type PProps = HTMLPropsWithStyle<HTMLDivElement> & {
   isLast?: boolean;
 };
@@ -550,43 +176,5 @@ export function P({ style, isLast, children, ...props }: PProps) {
   );
 }
 
-type FormErrorProps = {
-  style?: CSSProperties;
-  children?: ReactNode;
-};
-
-export function FormError({ style, children }: FormErrorProps) {
-  return (
-    <View style={[{ color: 'red', fontSize: 13 }, style]}>{children}</View>
-  );
-}
-
-type InitialFocusProps = {
-  children?: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement);
-};
-
-export function InitialFocus({ children }: InitialFocusProps) {
-  let node = useRef(null);
-
-  useEffect(() => {
-    if (node.current && !global.IS_DESIGN_MODE) {
-      // This is needed to avoid a strange interaction with
-      // `ScopeTab`, which doesn't allow it to be focused at first for
-      // some reason. Need to look into it.
-      setTimeout(() => {
-        if (node.current) {
-          node.current.focus();
-          node.current.setSelectionRange(0, 10000);
-        }
-      }, 0);
-    }
-  }, []);
-
-  if (typeof children === 'function') {
-    return children(node);
-  }
-  return cloneElement(children, { inputRef: node });
-}
-
 export * from './tooltips';
 export { useTooltip } from './tooltips';
diff --git a/packages/desktop-client/src/components/common/AlignedText.tsx b/packages/desktop-client/src/components/common/AlignedText.tsx
new file mode 100644
index 000000000..1636bf17c
--- /dev/null
+++ b/packages/desktop-client/src/components/common/AlignedText.tsx
@@ -0,0 +1,56 @@
+import { type ComponentProps } from 'react';
+
+import { type CSSProperties } from 'glamor';
+
+import Block from './Block';
+import View from './View';
+
+type AlignedTextProps = ComponentProps<typeof View> & {
+  left;
+  right;
+  style?: CSSProperties;
+  leftStyle?: CSSProperties;
+  rightStyle?: CSSProperties;
+  truncate?: 'left' | 'right';
+};
+export default function AlignedText({
+  left,
+  right,
+  style,
+  leftStyle,
+  rightStyle,
+  truncate = 'left',
+  ...nativeProps
+}: AlignedTextProps) {
+  const truncateStyle = {
+    textOverflow: 'ellipsis',
+    whiteSpace: 'nowrap',
+    overflow: 'hidden',
+  };
+
+  return (
+    <View
+      style={[{ flexDirection: 'row', alignItems: 'center' }, style]}
+      {...nativeProps}
+    >
+      <Block
+        style={[
+          { marginRight: 10 },
+          truncate === 'left' && truncateStyle,
+          leftStyle,
+        ]}
+      >
+        {left}
+      </Block>
+      <Block
+        style={[
+          { flex: 1, textAlign: 'right' },
+          truncate === 'right' && truncateStyle,
+          rightStyle,
+        ]}
+      >
+        {right}
+      </Block>
+    </View>
+  );
+}
diff --git a/packages/desktop-client/src/components/common/CustomSelect.tsx b/packages/desktop-client/src/components/common/CustomSelect.tsx
new file mode 100644
index 000000000..7114057fc
--- /dev/null
+++ b/packages/desktop-client/src/components/common/CustomSelect.tsx
@@ -0,0 +1,52 @@
+import {
+  ListboxInput,
+  ListboxButton,
+  ListboxPopover,
+  ListboxList,
+  ListboxOption,
+} from '@reach/listbox';
+import { type CSSProperties, css } from 'glamor';
+
+import ExpandArrow from '../../icons/v0/ExpandArrow';
+
+type CustomSelectProps = {
+  options: Array<[string, string]>;
+  value: string;
+  onChange?: (newValue: string) => void;
+  style?: CSSProperties;
+  disabledKeys?: string[];
+};
+
+export default function CustomSelect({
+  options,
+  value,
+  onChange,
+  style,
+  disabledKeys = [],
+}: CustomSelectProps) {
+  return (
+    <ListboxInput
+      value={value}
+      onChange={onChange}
+      style={{ lineHeight: '1em' }}
+    >
+      <ListboxButton
+        {...css([{ borderWidth: 0, padding: 5, borderRadius: 4 }, style])}
+        arrow={<ExpandArrow style={{ width: 7, height: 7, paddingTop: 3 }} />}
+      />
+      <ListboxPopover style={{ zIndex: 10000, outline: 0, borderRadius: 4 }}>
+        <ListboxList>
+          {options.map(([value, label]) => (
+            <ListboxOption
+              key={value}
+              value={value}
+              disabled={disabledKeys.includes(value)}
+            >
+              {label}
+            </ListboxOption>
+          ))}
+        </ListboxList>
+      </ListboxPopover>
+    </ListboxInput>
+  );
+}
diff --git a/packages/desktop-client/src/components/common/FormError.tsx b/packages/desktop-client/src/components/common/FormError.tsx
new file mode 100644
index 000000000..90854653f
--- /dev/null
+++ b/packages/desktop-client/src/components/common/FormError.tsx
@@ -0,0 +1,16 @@
+import { type ReactNode } from 'react';
+
+import { type CSSProperties } from 'glamor';
+
+import View from './View';
+
+type FormErrorProps = {
+  style?: CSSProperties;
+  children?: ReactNode;
+};
+
+export default function FormError({ style, children }: FormErrorProps) {
+  return (
+    <View style={[{ color: 'red', fontSize: 13 }, style]}>{children}</View>
+  );
+}
diff --git a/packages/desktop-client/src/components/common/InitialFocus.tsx b/packages/desktop-client/src/components/common/InitialFocus.tsx
new file mode 100644
index 000000000..7c6ad0a65
--- /dev/null
+++ b/packages/desktop-client/src/components/common/InitialFocus.tsx
@@ -0,0 +1,34 @@
+import {
+  type ReactElement,
+  type Ref,
+  cloneElement,
+  useEffect,
+  useRef,
+} from 'react';
+
+type InitialFocusProps = {
+  children?: ReactElement | ((node: Ref<HTMLInputElement>) => ReactElement);
+};
+
+export default function InitialFocus({ children }: InitialFocusProps) {
+  let node = useRef(null);
+
+  useEffect(() => {
+    if (node.current && !global.IS_DESIGN_MODE) {
+      // This is needed to avoid a strange interaction with
+      // `ScopeTab`, which doesn't allow it to be focused at first for
+      // some reason. Need to look into it.
+      setTimeout(() => {
+        if (node.current) {
+          node.current.focus();
+          node.current.setSelectionRange(0, 10000);
+        }
+      }, 0);
+    }
+  }, []);
+
+  if (typeof children === 'function') {
+    return children(node);
+  }
+  return cloneElement(children, { inputRef: node });
+}
diff --git a/packages/desktop-client/src/components/common/InputWithContent.tsx b/packages/desktop-client/src/components/common/InputWithContent.tsx
new file mode 100644
index 000000000..6d907be0f
--- /dev/null
+++ b/packages/desktop-client/src/components/common/InputWithContent.tsx
@@ -0,0 +1,72 @@
+import { type ComponentProps, type ReactNode, useState } from 'react';
+
+import { type CSSProperties } from 'glamor';
+
+import { colors } from '../../style';
+
+import Input, { defaultInputStyle } from './Input';
+import View from './View';
+
+type InputWithContentProps = ComponentProps<typeof Input> & {
+  leftContent: ReactNode;
+  rightContent: ReactNode;
+  inputStyle?: CSSProperties;
+  style?: CSSProperties;
+  getStyle?: (focused: boolean) => CSSProperties;
+};
+export default function InputWithContent({
+  leftContent,
+  rightContent,
+  inputStyle,
+  style,
+  getStyle,
+  ...props
+}: InputWithContentProps) {
+  let [focused, setFocused] = useState(false);
+
+  return (
+    <View
+      style={[
+        defaultInputStyle,
+        {
+          padding: 0,
+          flex: 1,
+          flexDirection: 'row',
+          alignItems: 'center',
+        },
+        focused && {
+          border: '1px solid ' + colors.b5,
+          boxShadow: '0 1px 1px ' + colors.b7,
+        },
+        style,
+        getStyle && getStyle(focused),
+      ]}
+    >
+      {leftContent}
+      <Input
+        {...props}
+        style={[
+          inputStyle,
+          {
+            flex: 1,
+            '&, &:focus, &:hover': {
+              border: 0,
+              backgroundColor: 'transparent',
+              boxShadow: 'none',
+              color: 'inherit',
+            },
+          },
+        ]}
+        onFocus={e => {
+          setFocused(true);
+          props.onFocus && props.onFocus(e);
+        }}
+        onBlur={e => {
+          setFocused(false);
+          props.onBlur && props.onBlur(e);
+        }}
+      />
+      {rightContent}
+    </View>
+  );
+}
diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx
new file mode 100644
index 000000000..8aa49d3e1
--- /dev/null
+++ b/packages/desktop-client/src/components/common/Menu.tsx
@@ -0,0 +1,181 @@
+import {
+  type ReactNode,
+  createElement,
+  useEffect,
+  useRef,
+  useState,
+} from 'react';
+
+import { colors } from '../../style';
+
+import Text from './Text';
+import View from './View';
+
+type KeybindingProps = {
+  keyName: ReactNode;
+};
+
+function Keybinding({ keyName }: KeybindingProps) {
+  return <Text style={{ fontSize: 10, color: colors.n6 }}>{keyName}</Text>;
+}
+
+type MenuItem = {
+  type?: string | symbol;
+  name: string;
+  disabled?: boolean;
+  icon?;
+  iconSize?: number;
+  text: string;
+  key?: string;
+};
+
+type MenuProps = {
+  header?: ReactNode;
+  footer?: ReactNode;
+  items: Array<MenuItem | typeof Menu.line>;
+  onMenuSelect;
+};
+
+export default function Menu({
+  header,
+  footer,
+  items: allItems,
+  onMenuSelect,
+}: MenuProps) {
+  let elRef = useRef(null);
+  let items = allItems.filter(x => x);
+  let [hoveredIndex, setHoveredIndex] = useState(null);
+
+  useEffect(() => {
+    const el = elRef.current;
+    el.focus();
+
+    let onKeyDown = e => {
+      let filteredItems = items.filter(
+        item => item && item !== Menu.line && item.type !== Menu.label,
+      );
+      let currentIndex = filteredItems.indexOf(items[hoveredIndex]);
+
+      let transformIndex = idx => items.indexOf(filteredItems[idx]);
+
+      switch (e.key) {
+        case 'ArrowUp':
+          e.preventDefault();
+          setHoveredIndex(
+            hoveredIndex === null
+              ? 0
+              : transformIndex(Math.max(currentIndex - 1, 0)),
+          );
+          break;
+        case 'ArrowDown':
+          e.preventDefault();
+          setHoveredIndex(
+            hoveredIndex === null
+              ? 0
+              : transformIndex(
+                  Math.min(currentIndex + 1, filteredItems.length - 1),
+                ),
+          );
+          break;
+        case 'Enter':
+          e.preventDefault();
+          const item = items[hoveredIndex];
+          if (hoveredIndex !== null && item !== Menu.line) {
+            onMenuSelect && onMenuSelect(item.name);
+          }
+          break;
+        default:
+      }
+    };
+
+    el.addEventListener('keydown', onKeyDown);
+
+    return () => {
+      el.removeEventListener('keydown', onKeyDown);
+    };
+  }, [hoveredIndex]);
+
+  return (
+    <View
+      style={{ outline: 'none', borderRadius: 4, overflow: 'hidden' }}
+      tabIndex={1}
+      innerRef={elRef}
+    >
+      {header}
+      {items.map((item, idx) => {
+        if (item === Menu.line) {
+          return (
+            <View key={idx} style={{ margin: '3px 0px' }}>
+              <View style={{ borderTop: '1px solid ' + colors.n10 }} />
+            </View>
+          );
+        } else if (item.type === Menu.label) {
+          return (
+            <Text
+              key={item.name}
+              style={{
+                color: colors.n6,
+                fontSize: 11,
+                lineHeight: '1em',
+                textTransform: 'uppercase',
+                margin: '3px 9px',
+                marginTop: 5,
+              }}
+            >
+              {item.name}
+            </Text>
+          );
+        }
+
+        let lastItem = items[idx - 1];
+
+        return (
+          <View
+            role="button"
+            key={item.name}
+            style={[
+              {
+                cursor: 'default',
+                padding: '9px 10px',
+                marginTop:
+                  idx === 0 ||
+                  lastItem === Menu.line ||
+                  lastItem.type === Menu.label
+                    ? 0
+                    : -3,
+                flexDirection: 'row',
+                alignItems: 'center',
+              },
+              item.disabled && { color: colors.n7 },
+              !item.disabled &&
+                hoveredIndex === idx && { backgroundColor: colors.n10 },
+            ]}
+            onMouseEnter={() => setHoveredIndex(idx)}
+            onMouseLeave={() => setHoveredIndex(null)}
+            onClick={e =>
+              !item.disabled && onMenuSelect && onMenuSelect(item.name)
+            }
+          >
+            {/* Force it to line up evenly */}
+            <Text style={{ lineHeight: 0 }}>
+              {item.icon &&
+                createElement(item.icon, {
+                  width: item.iconSize || 10,
+                  height: item.iconSize || 10,
+                  style: { marginRight: 7, width: 10 },
+                })}
+            </Text>
+            <Text>{item.text}</Text>
+            <View style={{ flex: 1 }} />
+            {item.key && <Keybinding keyName={item.key} />}
+          </View>
+        );
+      })}
+      {footer}
+    </View>
+  );
+}
+
+const MenuLine: unique symbol = Symbol('menu-line');
+Menu.line = MenuLine;
+Menu.label = Symbol('menu-label');
diff --git a/packages/desktop-client/src/components/common/Search.tsx b/packages/desktop-client/src/components/common/Search.tsx
new file mode 100644
index 000000000..c1b6bf832
--- /dev/null
+++ b/packages/desktop-client/src/components/common/Search.tsx
@@ -0,0 +1,43 @@
+import { type ChangeEvent, type Ref } from 'react';
+
+import { colors } from '../../style';
+
+import Input from './Input';
+
+type SearchProps = {
+  inputRef: Ref<HTMLInputElement>;
+  value: string;
+  onChange: (value: string) => unknown;
+  placeholder: string;
+  isInModal: boolean;
+  width?: number;
+};
+
+export default function Search({
+  inputRef,
+  value,
+  onChange,
+  placeholder,
+  isInModal,
+  width = 350,
+}: SearchProps) {
+  return (
+    <Input
+      inputRef={inputRef}
+      placeholder={placeholder}
+      value={value}
+      onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
+      style={{
+        width,
+        borderColor: isInModal ? null : 'transparent',
+        backgroundColor: isInModal ? null : colors.n11,
+        ':focus': isInModal
+          ? null
+          : {
+              backgroundColor: 'white',
+              '::placeholder': { color: colors.n8 },
+            },
+      }}
+    />
+  );
+}
diff --git a/upcoming-release-notes/1257.md b/upcoming-release-notes/1257.md
new file mode 100644
index 000000000..e0a8d2164
--- /dev/null
+++ b/upcoming-release-notes/1257.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Moving more components from `common.tsx` to separate files inside the `common` folder
-- 
GitLab