diff --git a/spec/__snapshots__/menu-button.spec.tsx.snap b/spec/__snapshots__/menu-button.spec.tsx.snap
new file mode 100644
index 00000000..14d45d47
--- /dev/null
+++ b/spec/__snapshots__/menu-button.spec.tsx.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Menu Button should show all elements 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/spec/__snapshots__/snippingTool.spec.tsx.snap b/spec/__snapshots__/snippingTool.spec.tsx.snap
index 824a19fe..a2d33c16 100644
--- a/spec/__snapshots__/snippingTool.spec.tsx.snap
+++ b/spec/__snapshots__/snippingTool.spec.tsx.snap
@@ -86,12 +86,38 @@ exports[`Snipping Tool should render correctly 1`] = `
+ Close
+
+
- Done
+ Add to chat
+
`;
diff --git a/spec/menu-button.spec.tsx b/spec/menu-button.spec.tsx
new file mode 100644
index 00000000..9f1bf954
--- /dev/null
+++ b/spec/menu-button.spec.tsx
@@ -0,0 +1,91 @@
+import { mount, shallow } from 'enzyme';
+import * as React from 'react';
+import { ScreenShotAnnotation } from '../src/common/ipcEvent';
+import MenuButton from '../src/renderer/components/menu-button';
+
+afterEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('Menu Button', () => {
+ //#region Logic
+ const waitForPromisesToResolve = () =>
+ new Promise((resolve) => setTimeout(resolve));
+ //#endregion
+
+ //#region Mock
+ const copyToClipboardFn = jest.fn();
+ const saveAsFn = jest.fn();
+ copyToClipboardFn.mockImplementation((event) => {
+ return event;
+ });
+ saveAsFn.mockImplementation((event) => {
+ return event;
+ });
+ const menuItem = [
+ {
+ name: 'Copy to clipboard',
+ event: ScreenShotAnnotation.COPY_TO_CLIPBOARD,
+ onClick: copyToClipboardFn,
+ dataTestId: 'COPY_TO_CLIPBOARD',
+ },
+ {
+ name: 'Save as',
+ event: ScreenShotAnnotation.SAVE_AS,
+ onClick: saveAsFn,
+ dataTestId: 'SAVE_AS',
+ },
+ ];
+ //#endregion
+
+ it('should show all elements', () => {
+ const wrapper = shallow(React.createElement(MenuButton));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should call event on click copy to clipboard', async () => {
+ const wrapper = mount(
+ ,
+ );
+
+ wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ wrapper
+ .find('[data-testid="snipping-tool_COPY_TO_CLIPBOARD"]')
+ .simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ expect(copyToClipboardFn).toBeCalledWith(
+ ScreenShotAnnotation.COPY_TO_CLIPBOARD,
+ );
+ });
+
+ it('should call event on click save as', async () => {
+ const wrapper = mount(
+ ,
+ );
+
+ wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ wrapper.find('[data-testid="snipping-tool_SAVE_AS"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ expect(saveAsFn).toBeCalledWith(ScreenShotAnnotation.SAVE_AS);
+ });
+
+ it('should disappear on click to other element', async () => {
+ const wrapper = mount(
+ ,
+ );
+
+ wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ expect(wrapper.find('.menu').exists()).toBeFalsy();
+ });
+});
diff --git a/spec/snippingTool.spec.tsx b/spec/snippingTool.spec.tsx
index 32ae19b7..f0837dd3 100644
--- a/spec/snippingTool.spec.tsx
+++ b/spec/snippingTool.spec.tsx
@@ -1,5 +1,14 @@
+jest.mock('save-svg-as-png', function () {
+ return {
+ svgAsPngUri: async function (svg) {
+ return Promise.resolve(svg);
+ },
+ };
+});
+
import { mount, shallow } from 'enzyme';
import * as React from 'react';
+import { ScreenShotAnnotation } from '../src/common/ipcEvent';
import SnippingTool from '../src/renderer/components/snipping-tool';
import { ipcRenderer } from './__mocks__/electron';
@@ -112,4 +121,61 @@ describe('Snipping Tool', () => {
screenSnippetPath: '',
});
});
+
+ it('should send upload-snippet event with correct data when clicked on copy to clipboard', async () => {
+ const wrapper = mount( );
+ const spy = jest.spyOn(ipcRenderer, 'send');
+ jest.spyOn(document, 'getElementById').mockImplementation((selector) => {
+ switch (selector) {
+ case 'annotate-area':
+ return '123';
+ default:
+ return '';
+ }
+ });
+
+ wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ wrapper
+ .find('[data-testid="snipping-tool_COPY_TO_CLIPBOARD"]')
+ .simulate('click');
+
+ wrapper.update();
+ await waitForPromisesToResolve();
+ expect(spy).toBeCalledWith(ScreenShotAnnotation.COPY_TO_CLIPBOARD, {
+ clipboard: '123',
+ });
+ });
+
+ it('should send upload-snippet event with correct data when clicked on save as', async () => {
+ const wrapper = mount( );
+ const spy = jest.spyOn(ipcRenderer, 'send');
+ jest.spyOn(document, 'getElementById').mockImplementation((selector) => {
+ switch (selector) {
+ case 'annotate-area':
+ return '123';
+ default:
+ return '';
+ }
+ });
+ wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ wrapper.find('[data-testid="snipping-tool_SAVE_AS"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ expect(spy).toBeCalledWith(ScreenShotAnnotation.SAVE_AS, {
+ clipboard: '123',
+ });
+ });
+
+ it('should send upload-snippet event with correct data when clicked on close', async () => {
+ const wrapper = mount( );
+ const spy = jest.spyOn(ipcRenderer, 'send');
+ wrapper.find('[data-testid="close-button"]').simulate('click');
+ wrapper.update();
+ await waitForPromisesToResolve();
+ expect(spy).toBeCalledWith(ScreenShotAnnotation.CLOSE);
+ });
});
diff --git a/src/app/screen-snippet-handler.ts b/src/app/screen-snippet-handler.ts
index efdea051..da889f72 100644
--- a/src/app/screen-snippet-handler.ts
+++ b/src/app/screen-snippet-handler.ts
@@ -1,6 +1,8 @@
import {
app,
BrowserWindow,
+ clipboard,
+ dialog,
ipcMain,
nativeImage,
WebContents,
@@ -20,6 +22,7 @@ import {
isWindowsOS,
} from '../common/env';
import { i18n } from '../common/i18n';
+import { ScreenShotAnnotation } from '../common/ipcEvent';
import { logger } from '../common/logger';
import {
analytics,
@@ -27,11 +30,18 @@ import {
ScreenSnippetActionTypes,
} from './analytics-handler';
import { updateAlwaysOnTop } from './window-actions';
-import { windowHandler } from './window-handler';
+import { ICustomBrowserWindow, windowHandler } from './window-handler';
import { windowExists } from './window-utils';
const readFile = util.promisify(fs.readFile);
+export interface IListItem {
+ name: string;
+ event: string;
+ dataTestId: string;
+ onClick: (eventName: string) => Promise;
+}
+
class ScreenSnippet {
private readonly tempDir: string;
private outputFilePath: string | undefined;
@@ -161,8 +171,18 @@ class ScreenSnippet {
}
windowHandler.closeSnippingToolWindow();
- windowHandler.createSnippingToolWindow(this.outputFilePath, dimensions);
+ const windowName = this.focusedWindow
+ ? (this.focusedWindow as ICustomBrowserWindow).winName
+ : '';
+ windowHandler.createSnippingToolWindow(
+ this.outputFilePath,
+ dimensions,
+ windowName,
+ );
this.uploadSnippet(webContents);
+ this.closeSnippet();
+ this.copyToClipboard();
+ this.saveAs();
return;
}
const {
@@ -194,7 +214,6 @@ class ScreenSnippet {
this.focusedWindow = BrowserWindow.getFocusedWindow();
try {
- await this.execCmd(this.captureUtil, []);
await this.verifyAndUpdateAlwaysOnTop();
} catch (error) {
await this.verifyAndUpdateAlwaysOnTop();
@@ -343,6 +362,124 @@ class ScreenSnippet {
},
);
}
+
+ /**
+ * Close the current snippet
+ */
+ private closeSnippet() {
+ ipcMain.once(ScreenShotAnnotation.CLOSE, async (_event) => {
+ try {
+ windowHandler.closeSnippingToolWindow();
+ await this.verifyAndUpdateAlwaysOnTop();
+ } catch (error) {
+ await this.verifyAndUpdateAlwaysOnTop();
+ logger.error(
+ `screen-snippet-handler: close window failed with error: ${error}!`,
+ );
+ }
+ });
+ }
+
+ /**
+ * Cancels a screen capture and closes the snippet window
+ */
+ private copyToClipboard() {
+ ipcMain.on(
+ ScreenShotAnnotation.COPY_TO_CLIPBOARD,
+ async (
+ _event,
+ copyToClipboardData: {
+ action: string;
+ clipboard: string;
+ },
+ ) => {
+ logger.info(`screen-snippet-handler: Copied!`);
+ this.focusedWindow = BrowserWindow.getFocusedWindow();
+
+ try {
+ const [, data] = copyToClipboardData.clipboard.split(',');
+
+ const buffer = Buffer.from(data, 'base64');
+ const img = nativeImage.createFromBuffer(buffer);
+
+ clipboard.writeImage(img);
+
+ await this.verifyAndUpdateAlwaysOnTop();
+ } catch (error) {
+ await this.verifyAndUpdateAlwaysOnTop();
+ logger.error(
+ `screen-snippet-handler: cannot copy, failed with error: ${error}!`,
+ );
+ }
+ },
+ );
+ }
+
+ /**
+ * Trigger save modal to save the snippet
+ */
+ private saveAs() {
+ ipcMain.on(
+ ScreenShotAnnotation.SAVE_AS,
+ async (
+ _event,
+ saveAsData: {
+ clipboard: string;
+ },
+ ) => {
+ const filePath = path.join(
+ this.tempDir,
+ 'symphonyImage-' + Date.now() + '.png',
+ );
+ const [, data] = saveAsData.clipboard.split(',');
+ const buffer = Buffer.from(data, 'base64');
+ const img = nativeImage.createFromBuffer(buffer);
+
+ const dialogResult = await dialog
+ .showSaveDialog(BrowserWindow.getFocusedWindow() as BrowserWindow, {
+ title: 'Select place to store your file',
+ defaultPath: filePath,
+ // defaultPath: path.join(__dirname, '../assets/'),
+ buttonLabel: 'Save',
+ // Restricting the user to only Text Files.
+ filters: [
+ {
+ name: 'Image file',
+ extensions: ['png'],
+ },
+ ],
+ properties: [],
+ })
+ .then((file) => {
+ // Stating whether dialog operation was cancelled or not.
+ if (!file.canceled && file.filePath) {
+ // Creating and Writing to the sample.txt file
+ fs.writeFile(file.filePath.toString(), img.toPNG(), (err) => {
+ if (err) {
+ throw logger.error(
+ `screen-snippet-handler: cannot save file, failed with error: ${err}!`,
+ );
+ }
+
+ logger.info(`screen-snippet-handler: modal save opened!`);
+ });
+ }
+
+ return file;
+ })
+ .catch((err) => {
+ logger.error(
+ `screen-snippet-handler: cannot save file, failed with error: ${err}!`,
+ );
+
+ return undefined;
+ });
+ if (dialogResult?.filePath) {
+ windowHandler.closeSnippingToolWindow();
+ }
+ },
+ );
+ }
}
const screenSnippet = new ScreenSnippet();
diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts
index 8ed8f7eb..0616aaac 100644
--- a/src/app/window-handler.ts
+++ b/src/app/window-handler.ts
@@ -20,6 +20,7 @@ import { format, parse } from 'url';
import { apiName, Themes, WindowTypes } from '../common/api-interface';
import { isDevEnv, isLinux, isMac, isWindowsOS } from '../common/env';
import { i18n, LocaleType } from '../common/i18n';
+import { ScreenShotAnnotation } from '../common/ipcEvent';
import { logger } from '../common/logger';
import {
calculatePercentage,
@@ -1101,6 +1102,7 @@ export class WindowHandler {
height: number;
width: number;
},
+ windowName: string,
): void {
// Prevents creating multiple instances
if (didVerifyAndRestoreWindow(this.snippingToolWindow)) {
@@ -1184,11 +1186,13 @@ export class WindowHandler {
toolWidth = scaledImageDimensions.width;
}
+ const selectedParentWindow = getWindowByName(windowName);
const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts(
{
width: toolWidth,
height: toolHeight,
- modal: false,
+ parent: selectedParentWindow,
+ modal: true,
alwaysOnTop: false,
resizable: false,
fullscreenable: false,
@@ -1270,6 +1274,9 @@ export class WindowHandler {
logger.info(
'window-handler, createSnippingToolWindow: Closing snipping window, attempting to delete temp snip image',
);
+ ipcMain.removeAllListeners(ScreenShotAnnotation.COPY_TO_CLIPBOARD);
+ ipcMain.removeAllListeners(ScreenShotAnnotation.SAVE_AS);
+ this.snippingToolWindow?.close();
this.deleteFile(snipImage);
this.removeWindow(opts.winKey);
this.screenPickerWindow = null;
diff --git a/src/common/ipcEvent.ts b/src/common/ipcEvent.ts
new file mode 100644
index 00000000..a34bb6cb
--- /dev/null
+++ b/src/common/ipcEvent.ts
@@ -0,0 +1,5 @@
+export enum ScreenShotAnnotation {
+ COPY_TO_CLIPBOARD = 'copy-to-clipboard',
+ SAVE_AS = 'save-as',
+ CLOSE = 'close-snippet',
+}
diff --git a/src/locale/en-US.json b/src/locale/en-US.json
index 59ded3b1..e2081545 100644
--- a/src/locale/en-US.json
+++ b/src/locale/en-US.json
@@ -160,7 +160,11 @@
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
},
"ScreenSnippet": {
- "Done": "Done",
+ "Add to chat": "ADD TO CHAT",
+ "Open menu": "Open menu",
+ "Copy to clipboard": "Copy to clipboard",
+ "Save as": "Save as",
+ "Close": "Close",
"Erase": "Erase",
"Highlight": "Highlight",
"Pen": "Pen",
@@ -236,4 +240,4 @@
"Allow once (risky)": "Allow once (risky)",
"Deny": "Deny",
"Invalid security certificate": "has an invalid security certificate."
-}
\ No newline at end of file
+}
diff --git a/src/locale/en.json b/src/locale/en.json
index 59ded3b1..e943b238 100644
--- a/src/locale/en.json
+++ b/src/locale/en.json
@@ -160,7 +160,11 @@
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
},
"ScreenSnippet": {
- "Done": "Done",
+ "Add to chat": "Add to chat",
+ "Open menu": "Open menu",
+ "Copy to clipboard": "Copy to clipboard",
+ "Save as": "Save as",
+ "Close": "Close",
"Erase": "Erase",
"Highlight": "Highlight",
"Pen": "Pen",
@@ -236,4 +240,4 @@
"Allow once (risky)": "Allow once (risky)",
"Deny": "Deny",
"Invalid security certificate": "has an invalid security certificate."
-}
\ No newline at end of file
+}
diff --git a/src/renderer/assets/single-chevron-down.svg b/src/renderer/assets/single-chevron-down.svg
new file mode 100644
index 00000000..f0c9dda0
--- /dev/null
+++ b/src/renderer/assets/single-chevron-down.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/renderer/components/menu-button.tsx b/src/renderer/components/menu-button.tsx
new file mode 100644
index 00000000..eb8f20a8
--- /dev/null
+++ b/src/renderer/components/menu-button.tsx
@@ -0,0 +1,120 @@
+// import { ipcRenderer } from 'electron';
+import * as React from 'react';
+import { IListItem } from '../../app/screen-snippet-handler';
+import { i18n } from '../../common/i18n-preload';
+
+const { useState, useEffect, useRef } = React;
+
+interface IMenuButtonProps {
+ listItems: IListItem[];
+ id;
+}
+
+const MenuButton: React.FunctionComponent = ({
+ listItems,
+ id,
+}) => {
+ //#region State
+ const [isDisplay, setDisplay] = useState(false);
+ const listRef = useRef(document.createElement('ul'));
+ const menuButtonRef = useRef(
+ document.createElement('button'),
+ );
+ //#endregion
+
+ //#region Variables
+ const testId = {
+ menu: `${id}_MENU_BUTTON`,
+ };
+ //#endregion
+
+ //#region Handlers
+ const onClickMenuButton = () => {
+ setDisplay(!isDisplay);
+ };
+ //#endregion
+
+ //#region UseEffect
+ useEffect(() => {
+ const isContainListElement = (ev) =>
+ listRef.current && listRef.current.contains(ev.target as HTMLElement);
+ const isContainMenuBtnElement = (ev) =>
+ menuButtonRef.current &&
+ menuButtonRef.current.contains(ev.target as HTMLElement);
+ window.addEventListener('click', (ev: MouseEvent) => {
+ if (!isContainListElement(ev) && !isContainMenuBtnElement(ev)) {
+ setDisplay(false);
+ }
+ });
+ window.addEventListener('keydown', (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setDisplay(false);
+ }
+ });
+
+ return () => {
+ window.removeEventListener('click', () => {
+ if (!isContainListElement || !isContainMenuBtnElement) {
+ setDisplay(false);
+ }
+ });
+ window.addEventListener('keydown', (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setDisplay(false);
+ }
+ });
+ };
+ });
+ //#endregion
+
+ const renderListItems = () => {
+ return listItems.map((listItem) => {
+ const sendClick = async () => {
+ await listItem.onClick(listItem.event);
+ setDisplay(false);
+ };
+
+ return (
+
+ {listItem.name}
+
+ );
+ });
+ };
+
+ const focusCls = isDisplay ? 'menu-button-focus' : '';
+
+ //#endregion
+
+ return (
+ <>
+
+
+
+
+ {isDisplay && (
+
+ )}
+
+ >
+ );
+};
+
+export default MenuButton;
diff --git a/src/renderer/components/snipping-tool.tsx b/src/renderer/components/snipping-tool.tsx
index 147c0eef..6a7c5cf3 100644
--- a/src/renderer/components/snipping-tool.tsx
+++ b/src/renderer/components/snipping-tool.tsx
@@ -2,12 +2,14 @@ import { ipcRenderer } from 'electron';
import * as React from 'react';
import { svgAsPngUri } from 'save-svg-as-png';
import { i18n } from '../../common/i18n-preload';
+import { ScreenShotAnnotation } from '../../common/ipcEvent';
import {
AnalyticsElements,
ScreenSnippetActionTypes,
} from './../../app/analytics-handler';
import AnnotateArea from './annotate-area';
import ColorPickerPill, { IColor } from './color-picker-pill';
+import MenuButton from './menu-button';
const { useState, useRef, useEffect, useLayoutEffect } = React;
@@ -92,6 +94,42 @@ const SnippingTool: React.FunctionComponent = ({
false,
);
+ const mergeImage = async () => {
+ const svg = document.getElementById('annotate-area');
+ const mergedImageData = svg ? await svgAsPngUri(svg, {}) : 'MERGE_FAIL';
+
+ return mergedImageData;
+ };
+
+ const onCopyToClipboard = async (eventName) => {
+ const img = await mergeImage();
+ ipcRenderer.send(eventName, {
+ clipboard: img,
+ });
+ };
+
+ const onSaveAs = async (eventName) => {
+ const img = await mergeImage();
+ ipcRenderer.send(eventName, {
+ clipboard: img,
+ });
+ };
+
+ const menuItem = [
+ {
+ name: i18n.t('Copy to clipboard', SNIPPING_TOOL_NAMESPACE)(),
+ event: ScreenShotAnnotation.COPY_TO_CLIPBOARD,
+ onClick: onCopyToClipboard,
+ dataTestId: 'COPY_TO_CLIPBOARD',
+ },
+ {
+ name: i18n.t('Save as', SNIPPING_TOOL_NAMESPACE)(),
+ event: ScreenShotAnnotation.SAVE_AS,
+ onClick: onSaveAs,
+ dataTestId: 'SAVE_AS',
+ },
+ ];
+
const getSnipImageData = (
{},
{
@@ -225,7 +263,7 @@ const SnippingTool: React.FunctionComponent = ({
return undefined;
};
- const done = async () => {
+ const addToChat = async () => {
const svg = document.getElementById('annotate-area');
const mergedImageData = svg
? await svgAsPngUri(document.getElementById('annotate-area'), {})
@@ -237,6 +275,10 @@ const SnippingTool: React.FunctionComponent = ({
ipcRenderer.send('upload-snippet', { screenSnippetPath, mergedImageData });
};
+ const onClose = async () => {
+ ipcRenderer.send(ScreenShotAnnotation.CLOSE);
+ };
+
// Removes focus styling from buttons when mouse is clicked
document.body.addEventListener('mousedown', () => {
document.body.classList.add('using-mouse');
@@ -350,12 +392,20 @@ const SnippingTool: React.FunctionComponent = ({
- {i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
+ {i18n.t('Close', SNIPPING_TOOL_NAMESPACE)()}
+
+ {i18n.t('Add to chat', SNIPPING_TOOL_NAMESPACE)()}
+
+
);
diff --git a/src/renderer/styles/snipping-tool.less b/src/renderer/styles/snipping-tool.less
index e6b836be..282c54fa 100644
--- a/src/renderer/styles/snipping-tool.less
+++ b/src/renderer/styles/snipping-tool.less
@@ -124,18 +124,42 @@ body.using-mouse :focus {
padding-top: 16px;
padding-bottom: 24px;
- .done-button {
+ .add-to-chat-button {
border: none;
border-radius: 16px;
font-size: 0.75rem;
font-weight: 600;
- background-color: #008eff;
- color: white;
+ background-color: @electricity-ui-50;
+ color: @vanilla-white;
cursor: pointer;
text-transform: uppercase;
- margin-right: 32px;
+ border-radius: 16px 0px 0px 16px;
height: 32px;
- width: 80px;
+ width: 102px;
+ }
+
+ .add-to-chat-button:hover {
+ background-color: @electricity-ui-60;
+ }
+
+ .close-button {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-style: normal;
+ font-weight: 600;
+ font-size: 0.875rem;
+ padding: 8px 16px;
+ width: 67px;
+ height: 32px;
+ border-radius: 16px;
+ font-family: @font-family;
+ color: @graphite-50;
+ text-transform: uppercase;
+ margin-right: 16px;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
}
}
}
@@ -172,3 +196,73 @@ body.using-mouse :focus {
width: 8px;
height: 8px;
}
+
+.menu-button {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 8px 8px 6px;
+ width: 30px;
+ height: 32px;
+ background: @electricity-ui-50;
+ border-radius: 0px 16px 16px 0px;
+ margin-left: 1px;
+ border: none;
+ cursor: pointer;
+}
+
+.menu-button:focus {
+ background: @electricity-ui-60;
+}
+
+.menu-button-focus {
+ background: @electricity-ui-60;
+}
+
+.chevron-down {
+ color: @text-color-primary;
+}
+
+.menu {
+ box-sizing: border-box;
+ top: -85px;
+ left: -113px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 14px 16px;
+ position: absolute;
+ width: 143px;
+ min-height: 80px;
+ background: @vanilla-white;
+ box-shadow: 0px 2px 4px rgba(5, 6, 6, 0.08), 0px 12px 28px rgba(5, 6, 6, 0.16);
+ border-radius: 8px;
+ margin: 0px;
+ cursor: pointer;
+}
+
+.menu-button-wrapper {
+ position: relative;
+ margin-right: 32px;
+}
+
+.general-font {
+ font-weight: 400;
+ font-size: 0.875rem;
+ line-height: 20px;
+ color: @graphite-80;
+ font-family: @font-family;
+}
+
+.list-item {
+ display: flex;
+ align-items: center;
+ list-style-type: none;
+ width: 100%;
+}
+
+.general-font:lang(ja-JP) {
+ font-family: @font-family-ja;
+}
diff --git a/src/renderer/styles/theme.less b/src/renderer/styles/theme.less
index 835639a3..46ccf9a8 100644
--- a/src/renderer/styles/theme.less
+++ b/src/renderer/styles/theme.less
@@ -9,9 +9,14 @@
@electricity-ui-05: #e9f2f9;
@electricity-ui-50: #0277d6;
@electricity-ui-30: #6eb9fd;
+@electricity-ui-60: #27588e;
+
@graphite-20: #cdcfd4;
@graphite-05: #f1f1f3;
@graphite-80: #27292c;
@graphite-30: #b0b3ba;
@graphite-40: #8f959e;
@graphite-90: #141618;
+@graphite-50: #717681;
+
+@vanilla-white: #fff;