Rework and move flashing view elements

Change-type: patch
Changelog-entry: Rework and move flashing view elements
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2020-06-15 19:34:31 +02:00
parent 9b71772e35
commit 76086a8f91
18 changed files with 261 additions and 330 deletions

View File

@ -21,11 +21,14 @@ import * as analytics from '../../modules/analytics';
import { SafeWebview } from '../safe-webview/safe-webview';
interface FeaturedProjectProps {
shouldShow: boolean;
onWebviewShow: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
}
interface FeaturedProjectState {
endpoint: string | null;
show: boolean;
}
export class FeaturedProject extends React.Component<
@ -34,14 +37,26 @@ export class FeaturedProject extends React.Component<
> {
constructor(props: FeaturedProjectProps) {
super(props);
this.state = { endpoint: null };
this.state = {
endpoint: null,
show: false,
};
}
public async componentDidMount() {
try {
const endpoint =
let endpoint =
(await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html';
const efpParams = {
borderRight: false,
darkBackground: true,
};
let params = '?';
for (const [param, value] of Object.entries(efpParams)) {
params += `${param}=${value}&`;
}
endpoint += params;
this.setState({ endpoint });
} catch (error) {
analytics.logException(error);
@ -49,8 +64,16 @@ export class FeaturedProject extends React.Component<
}
public render() {
const { style = {} } = this.props;
return this.state.endpoint ? (
<SafeWebview src={this.state.endpoint} {...this.props}></SafeWebview>
<SafeWebview
src={this.state.endpoint}
style={{
display: this.state.show ? 'block' : 'none',
...style,
}}
{...this.props}
></SafeWebview>
) : null;
}
}

View File

@ -14,25 +14,14 @@
* limitations under the License.
*/
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle, faCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import { Txt, Flex } from 'rendition';
import styled from 'styled-components';
import { left, position, space, top } from 'styled-system';
import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units';
import { Underline } from '../../styled-components';
const Div = styled.div<any>`
${position}
${top}
${left}
${space}
`;
export function FlashResults({
errors,
@ -50,15 +39,19 @@ export function FlashResults({
};
}) {
const allDevicesFailed = results.devices.successful === 0;
const effectiveSpeed = _.round(
bytesToMegabytes(
results.sourceMetadata.size /
(results.bytesWritten / results.averageFlashingSpeed),
),
1,
);
const effectiveSpeed = bytesToMegabytes(
results.sourceMetadata.size /
(results.bytesWritten / results.averageFlashingSpeed),
).toFixed(1);
return (
<Div position="absolute" left="153px" top="66px">
<Flex
flexDirection="column"
style={{
position: 'absolute',
left: '153px',
top: '66px',
}}
>
<Flex alignItems="center">
<FontAwesomeIcon
icon={faCheckCircle}
@ -73,24 +66,22 @@ export function FlashResults({
Flash Complete!
</Txt>
</Flex>
<Div className="results" mr="0" mb="0" ml="40px">
{_.map(results.devices, (quantity, type) => {
<Flex flexDirection="column" mr="0" mb="0" ml="40px">
{Object.keys(results.devices).map((type: 'failed' | 'successful') => {
const quantity = results.devices[type];
return quantity ? (
<Underline
<Flex
color="#fff"
alignItems="center"
tooltip={type === 'failed' ? errors : undefined}
key={type}
>
<div
key={type}
className={`target-status-line target-status-${type}`}
>
<span className="target-status-dot"></span>
<span className="target-status-quantity">{quantity}</span>
<span className="target-status-message">
{progress[type](quantity)}
</span>
</div>
</Underline>
<FontAwesomeIcon
color={type === 'failed' ? '#ff4444' : '#1ac135'}
icon={faCircle}
/>
<Txt ml={10}>{quantity}</Txt>
<Txt ml={10}>{progress[type](quantity)}</Txt>
</Flex>
) : null;
})}
{!allDevicesFailed && (
@ -109,7 +100,7 @@ export function FlashResults({
Effective speed: {effectiveSpeed} MB/s
</Txt>
)}
</Div>
</Div>
</Flex>
</Flex>
);
}

View File

@ -113,6 +113,9 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
warning={this.props.warning}
onClick={this.props.callback}
disabled={this.props.disabled}
style={{
marginTop: 30,
}}
>
Flash!
</StepButton>

View File

@ -15,51 +15,20 @@
*/
import * as React from 'react';
import { default as styled } from 'styled-components';
import { color } from 'styled-system';
import { SVGIcon } from '../svg-icon/svg-icon';
import { Flex, Txt } from 'rendition';
import DriveSvg from '../../../assets/drive.svg';
import ImageSvg from '../../../assets/image.svg';
const Div = styled.div`
position: absolute;
top: 45px;
left: 545px;
> span.step-name {
justify-content: flex-start;
> span {
margin-left: 10px;
}
> span:nth-child(2) {
font-weight: 500;
}
> span:nth-child(3) {
font-weight: 400;
font-style: italic;
}
}
.disabled {
opacity: 0.4;
}
`;
const Span = styled.span`
${color}
`;
import { SVGIcon } from '../svg-icon/svg-icon';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface ReducedFlashingInfosProps {
imageLogo: string;
imageName: string;
imageSize: string;
driveTitle: string;
shouldShow: boolean;
driveLabel: string;
style?: React.CSSProperties;
}
export class ReducedFlashingInfos extends React.Component<
@ -71,24 +40,36 @@ export class ReducedFlashingInfos extends React.Component<
}
public render() {
return this.props.shouldShow ? (
<Div>
<Span className="step-name">
return (
<Flex
flexDirection="column"
style={this.props.style ? this.props.style : undefined}
>
<Flex mb={16}>
<SVGIcon
disabled
width="20px"
width="21px"
height="21px"
contents={this.props.imageLogo}
fallback={<ImageSvg className="disabled" width="20px" />}
fallback={ImageSvg}
style={{ marginRight: 9 }}
/>
<Span>{this.props.imageName}</Span>
<Span color="#7e8085">{this.props.imageSize}</Span>
</Span>
<Txt
style={{ marginRight: 9 }}
tooltip={{ text: this.props.imageName, placement: 'right' }}
>
{middleEllipsis(this.props.imageName, 16)}
</Txt>
<Txt color="#7e8085">{this.props.imageSize}</Txt>
</Flex>
<Span className="step-name">
<DriveSvg className="disabled" width="20px" />
<Span>{this.props.driveTitle}</Span>
</Span>
</Div>
) : null;
<Flex>
<DriveSvg width="21px" height="21px" style={{ marginRight: 9 }} />
<Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
{middleEllipsis(this.props.driveTitle, 16)}
</Txt>
</Flex>
</Flex>
);
}
}

View File

@ -62,6 +62,7 @@ interface SafeWebviewProps {
refreshNow?: boolean;
// Webview lifecycle event
onWebviewShow?: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
}
interface SafeWebviewState {
@ -109,15 +110,18 @@ export class SafeWebview extends React.PureComponent<
}
public render() {
const {
style = {
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
},
} = this.props;
return (
<webview
ref={this.webviewRef}
partition={ELECTRON_SESSION}
style={{
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
}}
style={style}
/>
);
}

View File

@ -14,7 +14,11 @@
* limitations under the License.
*/
import { faFile, faLink } from '@fortawesome/free-solid-svg-icons';
import {
faFile,
faLink,
faExclamationTriangle,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { sourceDestination } from 'etcher-sdk';
import { ipcRenderer, IpcRendererEvent } from 'electron';
@ -480,7 +484,10 @@ export class SourceSelector extends React.Component<
>
<SVGIcon
contents={imageLogo}
fallback={<ImageSvg width="40px" />}
fallback={ImageSvg}
style={{
marginBottom: 30,
}}
/>
{hasImage ? (
@ -525,10 +532,10 @@ export class SourceSelector extends React.Component<
<Modal
titleElement={
<span>
<span
style={{ color: '#d9534f' }}
className="glyphicon glyphicon-exclamation-sign"
></span>{' '}
<FontAwesomeIcon
style={{ color: '#fca321' }}
icon={faExclamationTriangle}
/>{' '}
<span>{this.state.warning.title}</span>
</span>
}

View File

@ -39,13 +39,14 @@ function tryParseSVGContents(contents?: string): string | undefined {
interface SVGIconProps {
// List of embedded SVG contents to be tried in succession if any fails
contents: string;
fallback: JSX.Element;
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
// SVG image width unit
width?: string;
// SVG image height unit
height?: string;
// Should the element visually appear grayed out and disabled?
disabled?: boolean;
style?: React.CSSProperties;
}
/**
@ -54,17 +55,19 @@ interface SVGIconProps {
export class SVGIcon extends React.PureComponent<SVGIconProps> {
public render() {
const svgData = tryParseSVGContents(this.props.contents);
const { width, height, style = {} } = this.props;
style.width = width || DEFAULT_SIZE;
style.height = height || DEFAULT_SIZE;
if (svgData !== undefined) {
const width = this.props.width || DEFAULT_SIZE;
const height = this.props.height || DEFAULT_SIZE;
return (
<img
className={this.props.disabled ? 'disabled' : ''}
style={{ width, height }}
style={style}
src={svgData}
/>
);
}
return this.props.fallback;
const { fallback: FallbackSVG } = this.props;
return <FallbackSVG style={style} />;
}
}

View File

@ -15,10 +15,8 @@
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import * as React from 'react';
import { Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { Txt, Flex, FlexProps } from 'rendition';
import {
getDriveImageCompatibilityStatuses,
@ -33,10 +31,8 @@ import {
StepNameButton,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
float: ${({ float }) => float};
`;
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
interface TargetSelectorProps {
targets: any[];
@ -49,24 +45,26 @@ interface TargetSelectorProps {
image: Image;
}
function DriveCompatibilityWarning(props: {
function DriveCompatibilityWarning({
drive,
image,
...props
}: {
drive: DrivelistDrive;
image: Image;
}) {
} & FlexProps) {
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
props.drive,
props.image,
drive,
image,
);
if (compatibilityWarnings.length === 0) {
return null;
}
const messages = _.map(compatibilityWarnings, 'message');
const messages = compatibilityWarnings.map((warning) => warning.message);
return (
<Txt.span
className="glyphicon glyphicon-exclamation-sign"
ml={2}
tooltip={messages.join(', ')}
/>
<Flex tooltip={messages.join(', ')} {...props}>
<FontAwesomeIcon icon={faExclamationTriangle} />
</Flex>
);
}
@ -86,7 +84,11 @@ export function TargetSelector(props: TargetSelectorProps) {
</ChangeButton>
)}
<DetailsText>
<DriveCompatibilityWarning drive={target} image={props.image} />
<DriveCompatibilityWarning
drive={target}
image={props.image}
mr={2}
/>
{bytesToClosestUnit(target.size)}
</DetailsText>
</>
@ -104,15 +106,13 @@ export function TargetSelector(props: TargetSelectorProps) {
} ${bytesToClosestUnit(target.size)}`}
px={21}
>
<Txt.span>
<DriveCompatibilityWarning drive={target} image={props.image} />
<TargetDetail float="left">
{middleEllipsis(target.description, 14)}
</TargetDetail>
<TargetDetail float="right">
{bytesToClosestUnit(target.size)}
</TargetDetail>
</Txt.span>
<DriveCompatibilityWarning
drive={target}
image={props.image}
mr={2}
/>
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
<Txt>{bytesToClosestUnit(target.size)}</Txt>
</DetailsText>,
);
}

View File

@ -101,20 +101,3 @@
}
}
}
.inline-flex {
display: inline-flex;
}
.page-finish .tick {
/* hack(Shou): for some reason the height is stretched */
height: 24px;
width: 24px;
border: none;
padding: 0;
margin: 0 15px 0 0;
justify-content: center;
align-items: center;
display: flex;
font-size: 16px;
}

View File

@ -31,7 +31,7 @@ import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import DriveSvg from '../../../assets/drive.svg';
const getDriveListLabel = () => {
export const getDriveListLabel = () => {
return getSelectedDrives()
.map((drive: any) => {
return `${drive.description} (${drive.displayName})`;
@ -107,7 +107,13 @@ export const DriveSelector = ({
return (
<Flex flexDirection="column" alignItems="center">
<DriveSvg className={disabled ? 'disabled' : ''} width="40px" />
<DriveSvg
className={disabled ? 'disabled' : ''}
width="40px"
style={{
marginBottom: 30,
}}
/>
<TargetSelector
disabled={disabled}

View File

@ -34,6 +34,8 @@ import * as notification from '../../os/notification';
import { selectAllTargets } from './DriveSelector';
import FlashSvg from '../../../assets/flash.svg';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircle } from '@fortawesome/free-solid-svg-icons';
const COMPLETED_PERCENTAGE = 100;
const SPEED_PRECISION = 2;
@ -145,6 +147,8 @@ interface FlashStepProps {
goToSuccess: () => void;
source: SourceOptions;
isFlashing: boolean;
isWebviewShowing: boolean;
style?: React.CSSProperties;
// TODO: factorize
step: 'decompressing' | 'flashing' | 'verifying';
percentage: number;
@ -234,10 +238,17 @@ export class FlashStep extends React.PureComponent<
public render() {
return (
<>
<Flex flexDirection="column" alignItems="center">
<Flex
flexDirection="column"
alignItems="start"
style={this.props.style}
>
<FlashSvg
width="40px"
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
style={{
margin: '0 auto',
}}
/>
<ProgressButton
@ -274,17 +285,11 @@ export class FlashStep extends React.PureComponent<
)}
{Boolean(this.props.failed) && (
<div className="target-status-wrap">
<div className="target-status-line target-status-failed">
<span className="target-status-dot"></span>
<span className="target-status-quantity">
{this.props.failed}
</span>
<span className="target-status-message">
{messages.progress.failed(this.props.failed)}{' '}
</span>
</div>
</div>
<Flex color="#fff" alignItems="center" mt={35}>
<FontAwesomeIcon color="#ff4444" icon={faCircle} />
<Txt ml={10}>{this.props.failed}</Txt>
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
</Flex>
)}
</Flex>

View File

@ -41,11 +41,10 @@ import {
IconButton as BaseIcon,
ThemedProvider,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { bytesToClosestUnit } from '../../../../shared/units';
import { DriveSelector } from './DriveSelector';
import { DriveSelector, getDriveListLabel } from './DriveSelector';
import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
@ -106,6 +105,7 @@ interface MainPageStateFromStore {
imageSize: number;
imageName: string;
driveTitle: string;
driveLabel: string;
}
interface MainPageState {
@ -142,6 +142,7 @@ export class MainPage extends React.Component<
imageSize: selectionState.getImageSize(),
imageName: getImageBasename(),
driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(),
};
}
@ -156,6 +157,8 @@ export class MainPage extends React.Component<
const shouldDriveStepBeDisabled = !this.state.hasImage;
const shouldFlashStepBeDisabled =
!this.state.hasImage || !this.state.hasDrive;
const notFlashingOrSplitView =
!this.state.isFlashing || !this.state.isWebviewShowing;
return (
<>
<header
@ -164,6 +167,8 @@ export class MainPage extends React.Component<
width: '100%',
padding: '13px 14px',
textAlign: 'center',
position: 'relative',
zIndex: 1,
}}
>
<span
@ -213,49 +218,83 @@ export class MainPage extends React.Component<
/>
)}
<Flex m="110px 55px" justifyContent="space-between">
<SourceSelector
flashing={this.state.isFlashing}
afterSelected={(source: SourceOptions) => this.setState({ source })}
/>
<Flex
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between"
>
{notFlashingOrSplitView && (
<SourceSelector
flashing={this.state.isFlashing}
afterSelected={(source: SourceOptions) =>
this.setState({ source })
}
/>
)}
{(!this.state.isWebviewShowing || !this.state.isFlashing) && (
{notFlashingOrSplitView && (
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
)}
<DriveSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
{notFlashingOrSplitView && (
<DriveSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
)}
{(!this.state.isWebviewShowing || !this.state.isFlashing) && (
{notFlashingOrSplitView && (
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
)}
{this.state.isFlashing && this.state.isWebviewShowing && (
{this.state.isFlashing && (
<>
<Flex
style={{
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
display: this.state.isWebviewShowing ? 'block' : 'none',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
_.isNumber(this.state.imageSize)
? (bytesToClosestUnit(this.state.imageSize) as string)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
<FeaturedProject
shouldShow={this.state.isWebviewShowing}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
/>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={middleEllipsis(this.state.imageName, 16)}
imageSize={
_.isNumber(this.state.imageSize)
? (bytesToClosestUnit(this.state.imageSize) as string)
: ''
}
driveTitle={middleEllipsis(this.state.driveTitle, 16)}
shouldShow={
this.state.isFlashing && this.state.isWebviewShowing
}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
</>
)}
@ -264,13 +303,15 @@ export class MainPage extends React.Component<
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
source={this.state.source}
isFlashing={flashState.isFlashing()}
isFlashing={this.state.isFlashing}
isWebviewShowing={this.state.isWebviewShowing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
</>

View File

@ -17,73 +17,3 @@
.disabled {
opacity: $disabled-opacity;
}
.page-main {
flex: 1;
align-self: center;
margin: 20px;
}
.page-main > .col-xs {
height: 165px;
}
.page-main .relative {
position: relative;
}
.page-main .glyphicon {
vertical-align: text-top;
}
.page-main .step-name {
display: flex;
justify-content: center;
align-items: center;
height: 39px;
width: 100%;
font-weight: bold;
color: $palette-theme-primary-foreground;
}
.target-status-wrap {
display: flex;
position: absolute;
top: 62px;
flex-direction: column;
margin: 8px 28px;
align-items: flex-start;
}
.target-status-line {
display: flex;
align-items: baseline;
> .target-status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
&.target-status-successful > .target-status-dot {
background-color: $palette-theme-success-background;
}
&.target-status-failed > .target-status-dot {
background-color: $palette-theme-danger-background;
}
> .target-status-quantity {
color: white;
font-weight: bold;
}
> .target-status-message {
color: gray;
margin-left: 10px;
}
}
.space-vertical-large {
position: relative;
}

View File

@ -25,7 +25,6 @@ $disabled-opacity: 0.2;
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
@import "./modules/theme";
@import "./modules/space";
@import "../pages/main/styles/main";
@import "../pages/finish/styles/finish";
@import "./desktop";

View File

@ -1,55 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$spacing-large: 30px;
$spacing-medium: 15px;
$spacing-small: 10px;
$spacing-tiny: 5px;
.space-medium {
margin: $spacing-medium;
}
.space-vertical-medium {
margin-top: $spacing-medium;
margin-bottom: $spacing-medium;
}
.space-vertical-small {
margin-top: $spacing-small;
margin-bottom: $spacing-small;
}
.space-top-large {
margin-top: $spacing-large;
}
.space-vertical-large {
margin-top: $spacing-large;
margin-bottom: $spacing-large;
}
.space-bottom-medium {
margin-bottom: $spacing-medium;
}
.space-bottom-large {
margin-bottom: $spacing-large;
}
.space-right-tiny {
margin-right: $spacing-tiny;
}

View File

@ -21,6 +21,8 @@ import {
Modal as ModalBase,
Provider,
Txt,
Flex,
FlexProps,
} from 'rendition';
import styled from 'styled-components';
import { space } from 'styled-system';
@ -98,15 +100,13 @@ export const Footer = styled(Txt)`
font-size: 10px;
`;
export const Underline = styled(Txt.span)`
border-bottom: 1px dotted;
padding-bottom: 2px;
`;
export const DetailsText = styled(Txt.p)`
color: ${colors.dark.disabled.foreground};
margin-bottom: 0;
`;
export const DetailsText = (props: FlexProps) => (
<Flex
alignItems="center"
color={colors.dark.disabled.foreground}
{...props}
/>
);
export const Modal = styled((props) => {
return (

View File

@ -67,6 +67,16 @@ export const colors = {
export const theme = {
colors,
global: {
font: {
size: 16,
},
text: {
medium: {
size: 16,
},
},
},
button: {
border: {
width: '0',

View File

@ -19,12 +19,12 @@ import { Dictionary } from 'lodash';
export const progress: Dictionary<(quantity: number) => string> = {
successful: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Successful device${plural}`;
return `Successful target${plural}`;
},
failed: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Failed device${plural}`;
return `Failed target${plural}`;
},
};