mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
Merge pull request #1112 from psjostrom/SDA-2533-Add-Annotate-To-Screenshot
feat: SDA-2533 add annotate to screenshot
This commit is contained in:
commit
d5850e9edb
29
spec/__snapshots__/annotateArea.spec.tsx.snap
Normal file
29
spec/__snapshots__/annotateArea.spec.tsx.snap
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<AnnotateArea/> should render correctly 1`] = `
|
||||||
|
<svg
|
||||||
|
data-testid="annotate-area"
|
||||||
|
height={800}
|
||||||
|
id="annotate"
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseMove={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"cursor": "crosshair",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width={800}
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
className="SnippetImage"
|
||||||
|
height={800}
|
||||||
|
id="screenSnippet"
|
||||||
|
width={800}
|
||||||
|
x={0}
|
||||||
|
xlinkHref="very-nice-path"
|
||||||
|
y={0}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`;
|
174
spec/__snapshots__/colorPickerPill.spec.tsx.snap
Normal file
174
spec/__snapshots__/colorPickerPill.spec.tsx.snap
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<ColorPickerPill/> should render chosen dots as larger 1`] = `
|
||||||
|
<div
|
||||||
|
className="ColorPicker"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(0, 0, 0, 0.64)"
|
||||||
|
key="rgba(0, 0, 0, 0.64)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(0, 0, 0, 0.64)",
|
||||||
|
"border": undefined,
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "24px",
|
||||||
|
"width": "24px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(233, 0, 0, 0.64)"
|
||||||
|
key="rgba(233, 0, 0, 0.64)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(233, 0, 0, 0.64)",
|
||||||
|
"border": undefined,
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "8px",
|
||||||
|
"width": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ColorPickerPill/> should render correctly 1`] = `
|
||||||
|
<div
|
||||||
|
className="ColorPicker"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(0, 142, 255, 0.64)"
|
||||||
|
key="rgba(0, 142, 255, 0.64)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(0, 142, 255, 0.64)",
|
||||||
|
"border": undefined,
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "8px",
|
||||||
|
"width": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(38, 196, 58, 0.64)"
|
||||||
|
key="rgba(38, 196, 58, 0.64)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(38, 196, 58, 0.64)",
|
||||||
|
"border": undefined,
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "8px",
|
||||||
|
"width": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(246, 178, 2, 0.64)"
|
||||||
|
key="rgba(246, 178, 2, 0.64)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(246, 178, 2, 0.64)",
|
||||||
|
"border": undefined,
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "8px",
|
||||||
|
"width": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(233, 0, 0, 0.64)"
|
||||||
|
key="rgba(233, 0, 0, 0.64)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(233, 0, 0, 0.64)",
|
||||||
|
"border": undefined,
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "8px",
|
||||||
|
"width": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ColorPickerPill/> should render outlined dot if provided in props 1`] = `
|
||||||
|
<div
|
||||||
|
className="ColorPicker"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(246, 178, 2, 1)"
|
||||||
|
key="rgba(246, 178, 2, 1)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(246, 178, 2, 1)",
|
||||||
|
"border": undefined,
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "8px",
|
||||||
|
"width": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="EnclosingCircle"
|
||||||
|
data-testid="colorDot rgba(255, 255, 255, 1)"
|
||||||
|
key="rgba(255, 255, 255, 1)"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ColorDot"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(255, 255, 255, 1)",
|
||||||
|
"border": "solid 1px rgba(0, 0, 0, 1)",
|
||||||
|
"cursor": "pointer",
|
||||||
|
"height": "6px",
|
||||||
|
"width": "6px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -11,12 +11,14 @@ exports[`Snipping Tool should render correctly 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="ActionButton"
|
className="ActionButton"
|
||||||
|
data-testid="pen-button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"border": "2px solid rgba(0, 142, 255, 1)",
|
"border": "2px solid rgba(0, 142, 255, 1)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
title="Pen"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="../renderer/assets/snip-draw.svg"
|
src="../renderer/assets/snip-draw.svg"
|
||||||
@ -24,7 +26,9 @@ exports[`Snipping Tool should render correctly 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="ActionButton"
|
className="ActionButton"
|
||||||
|
data-testid="highlight-button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
title="Highlight"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="../renderer/assets/snip-highlight.svg"
|
src="../renderer/assets/snip-highlight.svg"
|
||||||
@ -32,7 +36,9 @@ exports[`Snipping Tool should render correctly 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="ActionButton"
|
className="ActionButton"
|
||||||
|
data-testid="erase-button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
title="Erase"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="../renderer/assets/snip-erase.svg"
|
src="../renderer/assets/snip-erase.svg"
|
||||||
@ -44,26 +50,39 @@ exports[`Snipping Tool should render correctly 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="ClearButton"
|
className="ClearButton"
|
||||||
|
data-testid="clear-button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
;
|
<main
|
||||||
<main>
|
style={
|
||||||
<div>
|
Object {
|
||||||
<img
|
"minHeight": 200,
|
||||||
alt="Screen snippet"
|
}
|
||||||
className="SnippetImage"
|
}
|
||||||
height={600}
|
>
|
||||||
src="Screen-Snippet"
|
<AnnotateArea
|
||||||
width={800}
|
chosenTool="PEN"
|
||||||
/>
|
highlightColor="rgba(0, 142, 255, 0.64)"
|
||||||
</div>
|
imageDimensions={
|
||||||
|
Object {
|
||||||
|
"height": 600,
|
||||||
|
"width": 800,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChange={[Function]}
|
||||||
|
paths={Array []}
|
||||||
|
penColor="rgba(0, 142, 255, 1)"
|
||||||
|
screenSnippetPath="Screen-Snippet"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<button
|
<button
|
||||||
|
className="DoneButton"
|
||||||
|
data-testid="done-button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
Done
|
Done
|
||||||
|
92
spec/annotateArea.spec.tsx
Normal file
92
spec/annotateArea.spec.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import * as React from 'react';
|
||||||
|
import AnnotateArea from '../src/renderer/components/annotate-area';
|
||||||
|
import { Tool } from '../src/renderer/components/snipping-tool';
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
paths: [],
|
||||||
|
highlightColor: 'rgba(233, 0, 0, 0.64)',
|
||||||
|
penColor: 'rgba(38, 196, 58, 1)',
|
||||||
|
onChange: jest.fn(),
|
||||||
|
imageDimensions: { width: 800, height: 800 },
|
||||||
|
chosenTool: Tool.pen,
|
||||||
|
screenSnippetPath: 'very-nice-path',
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('<AnnotateArea/>', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange when drawn on annotate area', () => {
|
||||||
|
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
|
||||||
|
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 });
|
||||||
|
wrapper.simulate('mouseup');
|
||||||
|
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct pen props if drawn drawn on annotate area with pen', () => {
|
||||||
|
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
|
||||||
|
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 });
|
||||||
|
wrapper.simulate('mouseup');
|
||||||
|
expect(defaultProps.onChange).toHaveBeenCalledWith([{
|
||||||
|
color: 'rgba(38, 196, 58, 1)',
|
||||||
|
key: 'path0',
|
||||||
|
points: [{ x: 0, y: 0 }],
|
||||||
|
shouldShow: true,
|
||||||
|
strokeWidth: 5,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct highlight props if drawn drawn on annotate area with highlight', () => {
|
||||||
|
const highlightProps = {
|
||||||
|
paths: [],
|
||||||
|
highlightColor: 'rgba(233, 0, 0, 0.64)',
|
||||||
|
penColor: 'rgba(38, 196, 58, 1)',
|
||||||
|
onChange: jest.fn(),
|
||||||
|
imageDimensions: { width: 800, height: 800 },
|
||||||
|
chosenTool: Tool.highlight,
|
||||||
|
screenSnippetPath: 'very-nice-path',
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<AnnotateArea {...highlightProps} />);
|
||||||
|
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 });
|
||||||
|
wrapper.simulate('mouseup');
|
||||||
|
expect(highlightProps.onChange).toHaveBeenCalledWith([{
|
||||||
|
color: 'rgba(233, 0, 0, 0.64)',
|
||||||
|
key: 'path0',
|
||||||
|
points: [{ x: 0, y: 0 }],
|
||||||
|
shouldShow: true,
|
||||||
|
strokeWidth: 28,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render path if path is provided in props', () => {
|
||||||
|
const pathProps = {
|
||||||
|
paths: [{
|
||||||
|
points: [{ x: 0, y: 0 }],
|
||||||
|
shouldShow: true,
|
||||||
|
strokeWidth: 5,
|
||||||
|
color: 'rgba(233, 0, 0, 0.64)',
|
||||||
|
key: 'path0',
|
||||||
|
}],
|
||||||
|
highlightColor: 'rgba(233, 0, 0, 0.64)',
|
||||||
|
penColor: 'rgba(38, 196, 58, 1)',
|
||||||
|
onChange: jest.fn(),
|
||||||
|
imageDimensions: { width: 800, height: 800 },
|
||||||
|
chosenTool: Tool.highlight,
|
||||||
|
screenSnippetPath: 'very-nice-path',
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<AnnotateArea {...pathProps} />);
|
||||||
|
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render any path if no path is provided in props', () => {
|
||||||
|
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
|
||||||
|
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
49
spec/colorPickerPill.spec.tsx
Normal file
49
spec/colorPickerPill.spec.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import * as React from 'react';
|
||||||
|
import ColorPickerPill from '../src/renderer/components/color-picker-pill';
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
availableColors: [
|
||||||
|
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
|
||||||
|
{ rgbaColor: 'rgba(38, 196, 58, 0.64)' },
|
||||||
|
{ rgbaColor: 'rgba(246, 178, 2, 0.64)' },
|
||||||
|
{ rgbaColor: 'rgba(233, 0, 0, 0.64)' },
|
||||||
|
],
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<ColorPickerPill/>', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange when clicked on a color dot', () => {
|
||||||
|
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
|
||||||
|
wrapper
|
||||||
|
.find('[data-testid="colorDot rgba(233, 0, 0, 0.64)"]')
|
||||||
|
.simulate('click');
|
||||||
|
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chosen dots as larger', () => {
|
||||||
|
const chosenColorProps = {
|
||||||
|
availableColors: [{ rgbaColor: 'rgba(0, 0, 0, 0.64)', chosen: true }, { rgbaColor: 'rgba(233, 0, 0, 0.64)' },
|
||||||
|
],
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<ColorPickerPill {...chosenColorProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render outlined dot if provided in props', () => {
|
||||||
|
const outlinedColorProps = {
|
||||||
|
availableColors: [{ rgbaColor: 'rgba(246, 178, 2, 1)' },
|
||||||
|
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' },
|
||||||
|
],
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<ColorPickerPill {...outlinedColorProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
@ -17,4 +17,16 @@ describe('Snipping Tool', () => {
|
|||||||
shallow(React.createElement(SnippingTool));
|
shallow(React.createElement(SnippingTool));
|
||||||
expect(spy).toBeCalledWith(snippingToolLabel, expect.any(Function));
|
expect(spy).toBeCalledWith(snippingToolLabel, expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render pen color picker when clicked on pen', () => {
|
||||||
|
const wrapper = shallow(React.createElement(SnippingTool));
|
||||||
|
wrapper.find('[data-testid="pen-button"]').simulate('click');
|
||||||
|
expect(wrapper.find('[data-testid="pen-colorpicker"]').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render highlight color picker when clicked on highlight', () => {
|
||||||
|
const wrapper = shallow(React.createElement(SnippingTool));
|
||||||
|
wrapper.find('[data-testid="highlight-button"]').simulate('click');
|
||||||
|
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -958,14 +958,14 @@ export class WindowHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parentWindow = BrowserWindow.getFocusedWindow();
|
const parentWindow = BrowserWindow.getFocusedWindow();
|
||||||
const MIN_HEIGHT = 312;
|
const MIN_HEIGHT = 320;
|
||||||
const MIN_WIDTH = 320;
|
const MIN_WIDTH = 312;
|
||||||
const CONTAINER_HEIGHT = 124;
|
const CONTAINER_HEIGHT = 120;
|
||||||
|
|
||||||
let windowHeight = dimensions?.height
|
let windowHeight = dimensions?.height
|
||||||
? dimensions.height + CONTAINER_HEIGHT
|
? dimensions.height + CONTAINER_HEIGHT
|
||||||
: 600;
|
: 320;
|
||||||
let windowWidth = dimensions?.width || 800;
|
let windowWidth = dimensions?.width || 312;
|
||||||
|
|
||||||
if (dimensions && dimensions.height && dimensions.height < MIN_HEIGHT) {
|
if (dimensions && dimensions.height && dimensions.height < MIN_HEIGHT) {
|
||||||
windowHeight = MIN_HEIGHT + CONTAINER_HEIGHT;
|
windowHeight = MIN_HEIGHT + CONTAINER_HEIGHT;
|
||||||
|
229
src/renderer/components/annotate-area.tsx
Normal file
229
src/renderer/components/annotate-area.tsx
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import { LazyBrush } from 'lazy-brush';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IImageDimensions, IPath, IPoint, Tool } from './snipping-tool';
|
||||||
|
|
||||||
|
const { useState } = React;
|
||||||
|
|
||||||
|
export interface ISvgPath {
|
||||||
|
svgPath: string;
|
||||||
|
key: string;
|
||||||
|
strokeWidth: number;
|
||||||
|
color: string;
|
||||||
|
shouldShow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAnnotateAreaProps {
|
||||||
|
paths: IPath[];
|
||||||
|
highlightColor: string;
|
||||||
|
penColor: string;
|
||||||
|
onChange: (paths: IPath[]) => void;
|
||||||
|
imageDimensions: IImageDimensions;
|
||||||
|
chosenTool: Tool;
|
||||||
|
screenSnippetPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lazy = new LazyBrush({
|
||||||
|
radius: 3,
|
||||||
|
enabled: true,
|
||||||
|
initialPoint: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
const TOP_MENU_HEIGHT = 48;
|
||||||
|
const MIN_ANNOTATE_AREA_HEIGHT = 200;
|
||||||
|
const MIN_ANNOTATE_AREA_WIDTH = 312;
|
||||||
|
const PEN_WIDTH = 5;
|
||||||
|
const HIGHLIGHT_WIDTH = 28;
|
||||||
|
|
||||||
|
const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, highlightColor, penColor, onChange, imageDimensions, chosenTool, screenSnippetPath }) => {
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
|
||||||
|
const maybeErasePath = (key: string) => {
|
||||||
|
// erase logic here
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrawing = () => {
|
||||||
|
if (isDrawing) {
|
||||||
|
setIsDrawing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
|
||||||
|
const getMousePosition = (e: React.MouseEvent) => {
|
||||||
|
// We need to offset for elements in the window that is not the annotate area
|
||||||
|
const x = imageDimensions.width >= MIN_ANNOTATE_AREA_WIDTH ? e.pageX : e.pageX - (MIN_ANNOTATE_AREA_WIDTH - imageDimensions.width) / 2;
|
||||||
|
const y = imageDimensions.height >= MIN_ANNOTATE_AREA_HEIGHT ? (e.pageY - TOP_MENU_HEIGHT) : (e.pageY - ((MIN_ANNOTATE_AREA_HEIGHT - imageDimensions.height) / 2) - TOP_MENU_HEIGHT);
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render and preparing render functions
|
||||||
|
|
||||||
|
const addHighlightPoint = (paths: IPath[], point: IPoint) => {
|
||||||
|
const activePath = paths[paths.length - 1];
|
||||||
|
const shouldShow = true;
|
||||||
|
const key = 'path' + paths.length;
|
||||||
|
if (!isDrawing) {
|
||||||
|
paths.push({
|
||||||
|
points: [point],
|
||||||
|
color: highlightColor,
|
||||||
|
strokeWidth: HIGHLIGHT_WIDTH,
|
||||||
|
shouldShow,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activePath.points.push(point);
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPenPoint = (paths: IPath[], point: IPoint) => {
|
||||||
|
const activePath = paths[paths.length - 1];
|
||||||
|
const shouldShow = true;
|
||||||
|
const key = 'path' + paths.length;
|
||||||
|
if (!isDrawing) {
|
||||||
|
paths.push({
|
||||||
|
points: [point],
|
||||||
|
color: penColor,
|
||||||
|
strokeWidth: PEN_WIDTH,
|
||||||
|
shouldShow,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activePath.points.push(point);
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPathPoint = (e: React.MouseEvent) => {
|
||||||
|
const p = [...paths];
|
||||||
|
const mousePos = getMousePosition(e);
|
||||||
|
lazy.update({ x: mousePos.x, y: mousePos.y });
|
||||||
|
const point: IPoint = lazy.getBrushCoordinates();
|
||||||
|
if (chosenTool === Tool.highlight) {
|
||||||
|
onChange(addHighlightPoint(p, point));
|
||||||
|
} else {
|
||||||
|
onChange(addPenPoint(p, point));
|
||||||
|
}
|
||||||
|
if (!isDrawing) {
|
||||||
|
setIsDrawing(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPath = (path: ISvgPath) => {
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
pointerEvents={path.shouldShow ? 'visiblePainted' : 'none'}
|
||||||
|
style={{ display: path.shouldShow ? 'block' : 'none' }}
|
||||||
|
key={path.key}
|
||||||
|
stroke={path.color}
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeWidth={path.strokeWidth || 5}
|
||||||
|
d={path.svgPath}
|
||||||
|
fill='none'
|
||||||
|
onClick={() => maybeErasePath(path.key)}
|
||||||
|
data-testid={path.key}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPaths = (paths: ISvgPath[]) => {
|
||||||
|
return paths.map((path) => renderPath(path));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSvgDot = (point: IPoint) => {
|
||||||
|
const { x, y } = point;
|
||||||
|
// This is the SVG path data for a dot at the location of x, y
|
||||||
|
return (
|
||||||
|
'M ' +
|
||||||
|
x +
|
||||||
|
' ' +
|
||||||
|
y +
|
||||||
|
' m -0.1, 0 a 0.1,0.1 0 1,0 0.2,0 a 0.1,0.1 0 1,0 -0.2,0'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSvgPath = (points: IPoint[]) => {
|
||||||
|
let stroke = '';
|
||||||
|
if (points && points.length > 0) {
|
||||||
|
// Start point of path
|
||||||
|
stroke = `M ${points[0].x} ${points[0].y}`;
|
||||||
|
let p1: IPoint;
|
||||||
|
let p2: IPoint;
|
||||||
|
let end: IPoint;
|
||||||
|
// Adding points from points array to SVG curve path
|
||||||
|
for (let i = 1; i < points.length - 2; i += 2) {
|
||||||
|
p1 = points[i];
|
||||||
|
p2 = points[i + 1];
|
||||||
|
end = points[i + 2];
|
||||||
|
stroke += ` C ${p1.x} ${p1.y}, ${p2.x} ${p2.y}, ${end.x} ${end.y}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stroke;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSvgPathData = (path: IPath) => {
|
||||||
|
const points = path.points;
|
||||||
|
const x = points[0].x;
|
||||||
|
const y = points[0].y;
|
||||||
|
let data: string;
|
||||||
|
// Since a path must got from point A to point B, we need at least two X and Y pairs to render something.
|
||||||
|
// Therefore we start with render a dot, so that the user gets visual feedback from only one X and Y pair.
|
||||||
|
data = getSvgDot({ x, y });
|
||||||
|
data += getSvgPath(points);
|
||||||
|
|
||||||
|
return {
|
||||||
|
svgPath: data,
|
||||||
|
key: path && path.key,
|
||||||
|
strokeWidth: path && path.strokeWidth,
|
||||||
|
color: path && path.color,
|
||||||
|
shouldShow: path && path.shouldShow,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSvgPathsData = (paths: IPath[]) => {
|
||||||
|
return paths.map((path) => getSvgPathData(path));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mouse tracking functions
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
if (chosenTool === Tool.eraser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addPathPoint(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: React.MouseEvent) => {
|
||||||
|
if (!isDrawing || chosenTool === Tool.eraser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addPathPoint(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
data-testid='annotate-area'
|
||||||
|
style={{ cursor: 'crosshair' }}
|
||||||
|
id='annotate'
|
||||||
|
width={imageDimensions.width}
|
||||||
|
height={imageDimensions.height}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={stopDrawing}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseLeave={stopDrawing}
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
id='screenSnippet'
|
||||||
|
xlinkHref={screenSnippetPath}
|
||||||
|
width={imageDimensions.width}
|
||||||
|
height={imageDimensions.height}
|
||||||
|
className='SnippetImage'
|
||||||
|
/>
|
||||||
|
{renderPaths(getSvgPathsData(paths))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnnotateArea;
|
@ -38,7 +38,7 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={color.rgbaColor}
|
key={color.rgbaColor}
|
||||||
className='enclosingCircle'
|
className='EnclosingCircle'
|
||||||
onClick={chooseColor}
|
onClick={chooseColor}
|
||||||
data-testid={'colorDot ' + color.rgbaColor}
|
data-testid={'colorDot ' + color.rgbaColor}
|
||||||
>
|
>
|
||||||
@ -50,14 +50,14 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
border: hasOutline ? border : undefined,
|
border: hasOutline ? border : undefined,
|
||||||
}}
|
}}
|
||||||
className='colorDot'
|
className='ColorDot'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='colorPicker'>
|
<div className='ColorPicker'>
|
||||||
{props.availableColors.map((color) => ColorDot(color))}
|
{props.availableColors.map((color) => ColorDot(color))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { i18n } from '../../common/i18n-preload';
|
import { i18n } from '../../common/i18n-preload';
|
||||||
|
import AnnotateArea from './annotate-area';
|
||||||
import ColorPickerPill, { IColor } from './color-picker-pill';
|
import ColorPickerPill, { IColor } from './color-picker-pill';
|
||||||
|
|
||||||
const { useState, useCallback, useRef, useEffect } = React;
|
const { useState, useRef, useEffect } = React;
|
||||||
|
|
||||||
enum Tool {
|
export enum Tool {
|
||||||
pen = 'PEN',
|
pen = 'PEN',
|
||||||
highlight = 'HIGHLIGHT',
|
highlight = 'HIGHLIGHT',
|
||||||
eraser = 'ERASER',
|
eraser = 'ERASER',
|
||||||
@ -32,6 +33,12 @@ export interface ISvgPath {
|
|||||||
shouldShow: boolean;
|
shouldShow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IImageDimensions {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_ANNOTATE_AREA_HEIGHT = 200;
|
||||||
const availablePenColors: IColor[] = [
|
const availablePenColors: IColor[] = [
|
||||||
{ rgbaColor: 'rgba(0, 0, 40, 1)' },
|
{ rgbaColor: 'rgba(0, 0, 40, 1)' },
|
||||||
{ rgbaColor: 'rgba(0, 142, 255, 1)' },
|
{ rgbaColor: 'rgba(0, 142, 255, 1)' },
|
||||||
@ -48,19 +55,15 @@ const availableHighlightColors: IColor[] = [
|
|||||||
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
|
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
|
||||||
|
|
||||||
const SnippingTool = () => {
|
const SnippingTool = () => {
|
||||||
// State and ref preparation functions
|
// State preparation functions
|
||||||
|
|
||||||
const [screenSnippet, setScreenSnippet] = useState('Screen-Snippet');
|
const [screenSnippetPath, setScreenSnippetPath] = useState('Screen-Snippet');
|
||||||
const [imageDimensions, setImageDimensions] = useState({
|
const [imageDimensions, setImageDimensions] = useState({
|
||||||
height: 600,
|
height: 600,
|
||||||
width: 800,
|
width: 800,
|
||||||
});
|
});
|
||||||
const [paths, setPaths] = useState<IPath[]>([]);
|
const [paths, setPaths] = useState<IPath[]>([]);
|
||||||
const [chosenTool, setChosenTool] = useState(Tool.pen);
|
const [chosenTool, setChosenTool] = useState(Tool.pen);
|
||||||
const [annotateAreaLocation, setAnnotateAreaLocation] = useState({
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
});
|
|
||||||
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)');
|
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)');
|
||||||
const [highlightColor, setHighlightColor] = useState(
|
const [highlightColor, setHighlightColor] = useState(
|
||||||
'rgba(0, 142, 255, 0.64)',
|
'rgba(0, 142, 255, 0.64)',
|
||||||
@ -74,7 +77,7 @@ const SnippingTool = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getSnipImageData = ({ }, { snipImage, height, width }) => {
|
const getSnipImageData = ({ }, { snipImage, height, width }) => {
|
||||||
setScreenSnippet(snipImage);
|
setScreenSnippetPath(snipImage);
|
||||||
setImageDimensions({ height, width });
|
setImageDimensions({ height, width });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,12 +89,6 @@ const SnippingTool = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const annotateRef = useCallback((domNode) => {
|
|
||||||
if (domNode) {
|
|
||||||
setAnnotateAreaLocation(domNode.getBoundingClientRect());
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Hook that alerts clicks outside of the passed ref
|
// Hook that alerts clicks outside of the passed ref
|
||||||
const useClickOutsideExaminer = (
|
const useClickOutsideExaminer = (
|
||||||
colorPickerRf: React.RefObject<HTMLDivElement>,
|
colorPickerRf: React.RefObject<HTMLDivElement>,
|
||||||
@ -151,23 +148,11 @@ const SnippingTool = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
const updPaths = [...paths];
|
// Clear logic here
|
||||||
updPaths.map((p) => {
|
|
||||||
p.shouldShow = false;
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
setPaths(updPaths);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
|
|
||||||
const getMousePosition = (e: React.MouseEvent) => {
|
|
||||||
return {
|
|
||||||
x: e.pageX - annotateAreaLocation.left,
|
|
||||||
y: e.pageY - annotateAreaLocation.top,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const markChosenColor = (colors: IColor[], chosenColor: string) => {
|
const markChosenColor = (colors: IColor[], chosenColor: string) => {
|
||||||
return colors.map((color) => {
|
return colors.map((color) => {
|
||||||
if (color.rgbaColor === chosenColor) {
|
if (color.rgbaColor === chosenColor) {
|
||||||
@ -192,9 +177,8 @@ const SnippingTool = () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const done = (e) => {
|
const done = () => {
|
||||||
getMousePosition(e);
|
ipcRenderer.send('upload-snippet', screenSnippetPath);
|
||||||
ipcRenderer.send('upload-snippet', screenSnippet);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -202,39 +186,50 @@ const SnippingTool = () => {
|
|||||||
<header>
|
<header>
|
||||||
<div className='DrawActions'>
|
<div className='DrawActions'>
|
||||||
<button
|
<button
|
||||||
|
data-testid='pen-button'
|
||||||
style={getBorderStyle(Tool.pen)}
|
style={getBorderStyle(Tool.pen)}
|
||||||
className='ActionButton'
|
className='ActionButton'
|
||||||
onClick={usePen}
|
onClick={usePen}
|
||||||
|
title={i18n.t('Pen', SNIPPING_TOOL_NAMESPACE)()}
|
||||||
>
|
>
|
||||||
<img src='../renderer/assets/snip-draw.svg' />
|
<img src='../renderer/assets/snip-draw.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
data-testid='highlight-button'
|
||||||
style={getBorderStyle(Tool.highlight)}
|
style={getBorderStyle(Tool.highlight)}
|
||||||
className='ActionButton'
|
className='ActionButton'
|
||||||
onClick={useHighlight}
|
onClick={useHighlight}
|
||||||
|
title={i18n.t('Highlight', SNIPPING_TOOL_NAMESPACE)()}
|
||||||
>
|
>
|
||||||
<img src='../renderer/assets/snip-highlight.svg' />
|
<img src='../renderer/assets/snip-highlight.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
data-testid='erase-button'
|
||||||
style={getBorderStyle(Tool.eraser)}
|
style={getBorderStyle(Tool.eraser)}
|
||||||
className='ActionButton'
|
className='ActionButton'
|
||||||
onClick={useEraser}
|
onClick={useEraser}
|
||||||
|
title={i18n.t('Erase', SNIPPING_TOOL_NAMESPACE)()}
|
||||||
>
|
>
|
||||||
<img src='../renderer/assets/snip-erase.svg' />
|
<img src='../renderer/assets/snip-erase.svg' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className='ClearActions'>
|
<div className='ClearActions'>
|
||||||
<button className='ClearButton' onClick={clear}>
|
<button
|
||||||
|
data-testid='clear-button'
|
||||||
|
className='ClearButton'
|
||||||
|
onClick={clear}
|
||||||
|
>
|
||||||
{i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
|
{i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>;
|
</header>
|
||||||
|
|
||||||
{
|
{
|
||||||
shouldRenderPenColorPicker && (
|
shouldRenderPenColorPicker && (
|
||||||
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
|
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
|
||||||
<div style={{ position: 'relative', left: '-50%' }}>
|
<div style={{ position: 'relative', left: '-50%' }}>
|
||||||
<ColorPickerPill
|
<ColorPickerPill
|
||||||
|
data-testid='pen-colorpicker'
|
||||||
availableColors={markChosenColor(availablePenColors, penColor)}
|
availableColors={markChosenColor(availablePenColors, penColor)}
|
||||||
onChange={penColorChosen}
|
onChange={penColorChosen}
|
||||||
/>
|
/>
|
||||||
@ -247,6 +242,7 @@ const SnippingTool = () => {
|
|||||||
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
|
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
|
||||||
<div style={{ position: 'relative', left: '-50%' }}>
|
<div style={{ position: 'relative', left: '-50%' }}>
|
||||||
<ColorPickerPill
|
<ColorPickerPill
|
||||||
|
data-testid='highlight-colorpicker'
|
||||||
availableColors={markChosenColor(
|
availableColors={markChosenColor(
|
||||||
availableHighlightColors,
|
availableHighlightColors,
|
||||||
highlightColor,
|
highlightColor,
|
||||||
@ -258,20 +254,22 @@ const SnippingTool = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<main>
|
<main style={{ minHeight: MIN_ANNOTATE_AREA_HEIGHT }}>
|
||||||
<div ref={annotateRef}>
|
<AnnotateArea
|
||||||
<img
|
paths={paths}
|
||||||
src={screenSnippet}
|
highlightColor={highlightColor}
|
||||||
width={imageDimensions.width}
|
penColor={penColor}
|
||||||
height={imageDimensions.height}
|
onChange={setPaths}
|
||||||
className='SnippetImage'
|
imageDimensions={imageDimensions}
|
||||||
alt={i18n.t('Screen snippet', SNIPPING_TOOL_NAMESPACE)()}
|
screenSnippetPath={screenSnippetPath}
|
||||||
/>
|
chosenTool={chosenTool}
|
||||||
</div>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<button onClick={done}>
|
<button
|
||||||
|
data-testid='done-button'
|
||||||
|
className='DoneButton'
|
||||||
|
onClick={done}>
|
||||||
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
|
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -15,6 +15,13 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
&:focus {
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.SnippingTool:lang(ja-JP) {
|
.SnippingTool:lang(ja-JP) {
|
||||||
font-family: @font-family-ja;
|
font-family: @font-family-ja;
|
||||||
|
|
||||||
@ -43,8 +50,8 @@ body {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4px 0;
|
align-items: center;
|
||||||
max-height: 48px;
|
height: 48px;
|
||||||
|
|
||||||
.ActionButton {
|
.ActionButton {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@ -65,25 +72,21 @@ body {
|
|||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ClearButton {
|
.ClearButton {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 4px 10px;
|
|
||||||
border: 2px solid #7c7f86;
|
border: 2px solid #7c7f86;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: #7c7f86;
|
color: #7c7f86;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
margin-left: 24px;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
height: 24px;
|
||||||
|
width: 68px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DrawActions {
|
.DrawActions {
|
||||||
@ -95,22 +98,20 @@ body {
|
|||||||
|
|
||||||
.ClearActions {
|
.ClearActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: none;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 24px;
|
right: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.ActionButton {
|
|
||||||
img {
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
@ -119,6 +120,8 @@ body {
|
|||||||
rgba(255, 255, 255, 0.96)
|
rgba(255, 255, 255, 0.96)
|
||||||
),
|
),
|
||||||
#525760;
|
#525760;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.SnippetImage {
|
.SnippetImage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -129,18 +132,17 @@ body {
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 2px 32px 4px 0;
|
height: 72px;
|
||||||
max-height: 72px;
|
|
||||||
|
|
||||||
button {
|
.DoneButton {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 8px 24px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
@ -148,27 +150,18 @@ body {
|
|||||||
color: rgba(255, 255, 255, 0.96);
|
color: rgba(255, 255, 255, 0.96);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 10px 30px 4px 0;
|
margin-right: 32px;
|
||||||
|
height: 32px;
|
||||||
|
width: 80px;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 10px rgba(61, 162, 253, 1);
|
box-shadow: 0 0 10px rgba(61, 162, 253, 1);
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 400px) {
|
|
||||||
header {
|
|
||||||
.ClearActions {
|
|
||||||
position: relative;
|
|
||||||
right: auto;
|
|
||||||
margin-left: 24px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorPicker {
|
.ColorPicker {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
@ -181,7 +174,7 @@ body {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enclosingCircle {
|
.EnclosingCircle {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
@ -196,7 +189,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorDot {
|
.ColorDot {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex: none;
|
flex: none;
|
||||||
order: 0;
|
order: 0;
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
true,
|
true,
|
||||||
650
|
650
|
||||||
],
|
],
|
||||||
|
"no-shadowed-variable": false,
|
||||||
"no-trailing-whitespace": true,
|
"no-trailing-whitespace": true,
|
||||||
"no-duplicate-variable": true,
|
"no-duplicate-variable": true,
|
||||||
"no-var-keyword": true,
|
"no-var-keyword": true,
|
||||||
|
Loading…
Reference in New Issue
Block a user