mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 23:37:18 +00:00
Rework target selector modal
Change-type: patch Changelog-entry: Rework target selector modal Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
parent
f8cc7c36b4
commit
71c7fbd3a2
@ -1,280 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as React from 'react';
|
|
||||||
import { Modal } from 'rendition';
|
|
||||||
|
|
||||||
import {
|
|
||||||
COMPATIBILITY_STATUS_TYPES,
|
|
||||||
getDriveImageCompatibilityStatuses,
|
|
||||||
hasListDriveImageCompatibilityStatus,
|
|
||||||
isDriveValid,
|
|
||||||
} from '../../../../shared/drive-constraints';
|
|
||||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
|
||||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
|
||||||
import * as selectionState from '../../models/selection-state';
|
|
||||||
import { store } from '../../models/store';
|
|
||||||
import * as analytics from '../../modules/analytics';
|
|
||||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
|
||||||
|
|
||||||
import RaspberrypiSvg from '../../../assets/raspberrypi.svg';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Determine if we can change a drive's selection state
|
|
||||||
*/
|
|
||||||
function shouldChangeDriveSelectionState(drive: DrivelistDrive) {
|
|
||||||
return isDriveValid(drive, selectionState.getImage());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Toggle a drive selection
|
|
||||||
*/
|
|
||||||
function toggleDrive(drive: DrivelistDrive) {
|
|
||||||
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive);
|
|
||||||
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
analytics.logEvent('Toggle drive', {
|
|
||||||
drive,
|
|
||||||
previouslySelected: selectionState.isDriveSelected(drive.device),
|
|
||||||
});
|
|
||||||
|
|
||||||
selectionState.toggleDrive(drive.device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get a drive's compatibility status object(s)
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Given a drive, return its compatibility status with the selected image,
|
|
||||||
* containing the status type (ERROR, WARNING), and accompanying
|
|
||||||
* status message.
|
|
||||||
*/
|
|
||||||
function getDriveStatuses(
|
|
||||||
drive: DrivelistDrive,
|
|
||||||
): Array<{ type: number; message: string }> {
|
|
||||||
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyboardToggleDrive(
|
|
||||||
drive: DrivelistDrive,
|
|
||||||
event: React.KeyboardEvent<HTMLDivElement>,
|
|
||||||
) {
|
|
||||||
const ENTER = 13;
|
|
||||||
const SPACE = 32;
|
|
||||||
if (_.includes([ENTER, SPACE], event.keyCode)) {
|
|
||||||
toggleDrive(drive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DriverlessDrive {
|
|
||||||
link: string;
|
|
||||||
linkTitle: string;
|
|
||||||
linkMessage: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DriveSelectorModal({ close }: { close: () => void }) {
|
|
||||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
|
||||||
const [missingDriversModal, setMissingDriversModal] = React.useState(
|
|
||||||
defaultMissingDriversModalState,
|
|
||||||
);
|
|
||||||
const [drives, setDrives] = React.useState(getDrives());
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const unsubscribe = store.subscribe(() => {
|
|
||||||
setDrives(getDrives());
|
|
||||||
});
|
|
||||||
return unsubscribe;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Prompt the user to install missing usbboot drivers
|
|
||||||
*/
|
|
||||||
function installMissingDrivers(drive: {
|
|
||||||
link: string;
|
|
||||||
linkTitle: string;
|
|
||||||
linkMessage: string;
|
|
||||||
}) {
|
|
||||||
if (drive.link) {
|
|
||||||
analytics.logEvent('Open driver link modal', {
|
|
||||||
url: drive.link,
|
|
||||||
});
|
|
||||||
setMissingDriversModal({ drive });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Select a drive and close the modal
|
|
||||||
*/
|
|
||||||
async function selectDriveAndClose(drive: DrivelistDrive) {
|
|
||||||
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(
|
|
||||||
drive,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
selectionState.selectDrive(drive.device);
|
|
||||||
|
|
||||||
analytics.logEvent('Drive selected (double click)');
|
|
||||||
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasStatus = hasListDriveImageCompatibilityStatus(
|
|
||||||
selectionState.getSelectedDrives(),
|
|
||||||
selectionState.getImage(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="modal-drive-selector-modal"
|
|
||||||
titleElement="Select a Drive"
|
|
||||||
done={close}
|
|
||||||
action="Continue"
|
|
||||||
primaryButtonProps={{
|
|
||||||
primary: !hasStatus,
|
|
||||||
warning: hasStatus,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ul
|
|
||||||
style={{
|
|
||||||
height: '210px',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
overflowY: 'auto',
|
|
||||||
padding: '0px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{_.map(drives, (drive, index) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={`item-${drive.displayName}`}
|
|
||||||
className="list-group-item"
|
|
||||||
// @ts-ignore (FIXME: not a valid <li> attribute but used by css rule)
|
|
||||||
disabled={!isDriveValid(drive, selectionState.getImage())}
|
|
||||||
onDoubleClick={() => selectDriveAndClose(drive)}
|
|
||||||
onClick={() => toggleDrive(drive)}
|
|
||||||
>
|
|
||||||
{drive.icon === 'raspberrypi' && (
|
|
||||||
<RaspberrypiSvg
|
|
||||||
className="list-group-item-section"
|
|
||||||
width="25"
|
|
||||||
height="30"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="list-group-item-section list-group-item-section-expanded"
|
|
||||||
tabIndex={15 + index}
|
|
||||||
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}
|
|
||||||
>
|
|
||||||
<h6 className="list-group-item-heading">
|
|
||||||
{drive.description}
|
|
||||||
{drive.size && (
|
|
||||||
<span className="word-keep">
|
|
||||||
{' '}
|
|
||||||
- {bytesToClosestUnit(drive.size)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</h6>
|
|
||||||
{!drive.link && (
|
|
||||||
<p className="list-group-item-text">{drive.displayName}</p>
|
|
||||||
)}
|
|
||||||
{drive.link && (
|
|
||||||
<p className="list-group-item-text">
|
|
||||||
{drive.displayName} -{' '}
|
|
||||||
<b>
|
|
||||||
<a onClick={() => installMissingDrivers(drive)}>
|
|
||||||
{drive.linkCTA}
|
|
||||||
</a>
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<footer className="list-group-item-footer">
|
|
||||||
{_.map(getDriveStatuses(drive), (status, idx) => {
|
|
||||||
const className = {
|
|
||||||
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
|
|
||||||
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={`${drive.displayName}-status-${idx}`}
|
|
||||||
className={`label ${className[status.type]}`}
|
|
||||||
>
|
|
||||||
{status.message}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</footer>
|
|
||||||
{Boolean(drive.progress) && (
|
|
||||||
<progress
|
|
||||||
className="drive-init-progress"
|
|
||||||
value={drive.progress}
|
|
||||||
max="100"
|
|
||||||
></progress>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isDriveValid(drive, selectionState.getImage()) && (
|
|
||||||
<span
|
|
||||||
className="list-group-item-section tick tick--success"
|
|
||||||
// @ts-ignore (FIXME: not a valid <span> attribute but used by css rule)
|
|
||||||
disabled={!selectionState.isDriveSelected(drive.device)}
|
|
||||||
></span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!hasAvailableDrives() && (
|
|
||||||
<li className="list-group-item">
|
|
||||||
<div>
|
|
||||||
<b>Connect a drive!</b>
|
|
||||||
<div>No removable drive detected.</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{missingDriversModal.drive !== undefined && (
|
|
||||||
<Modal
|
|
||||||
width={400}
|
|
||||||
title={missingDriversModal.drive.linkTitle}
|
|
||||||
cancel={() => setMissingDriversModal({})}
|
|
||||||
done={() => {
|
|
||||||
try {
|
|
||||||
if (missingDriversModal.drive !== undefined) {
|
|
||||||
openExternal(missingDriversModal.drive.link);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
analytics.logException(error);
|
|
||||||
} finally {
|
|
||||||
setMissingDriversModal({});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
action={'Yes, continue'}
|
|
||||||
cancelButtonProps={{
|
|
||||||
children: 'Cancel',
|
|
||||||
}}
|
|
||||||
children={
|
|
||||||
missingDriversModal.drive.linkMessage ||
|
|
||||||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
|
||||||
}
|
|
||||||
></Modal>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,113 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-drive-selector-modal .modal-content {
|
|
||||||
width: 315px;
|
|
||||||
height: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-drive-selector-modal .modal-body {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-drive-selector-modal .list-group-item[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-drive-selector-modal {
|
|
||||||
|
|
||||||
.list-group-item-footer:has(span) {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-heading,
|
|
||||||
.list-group-item-text {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-left: 0;
|
|
||||||
border-right: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
border-color: darken($palette-theme-light-background, 7%);
|
|
||||||
padding: 12px 0;
|
|
||||||
|
|
||||||
.list-group-item-section-expanded {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-section + .list-group-item-section {
|
|
||||||
margin-left: 10px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .tick {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] .list-group-item-heading {
|
|
||||||
color: $palette-theme-light-soft-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drive-init-progress {
|
|
||||||
appearance: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 2.5px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drive-init-progress::-webkit-progress-bar {
|
|
||||||
background-color: $palette-theme-default-background;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drive-init-progress::-webkit-progress-value {
|
|
||||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
|
||||||
background-color: $palette-theme-primary-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-heading {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-text {
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 11px;
|
|
||||||
color: $palette-theme-light-soft-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-keep {
|
|
||||||
word-break: keep-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge, Checkbox, Modal } from 'rendition';
|
import { Checkbox, Modal } from 'rendition';
|
||||||
|
|
||||||
import { version } from '../../../../../package.json';
|
import { version } from '../../../../../package.json';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
@ -92,23 +92,6 @@ async function getSettingsList(): Promise<Setting[]> {
|
|||||||
name: 'updatesEnabled',
|
name: 'updatesEnabled',
|
||||||
label: 'Auto-updates enabled',
|
label: 'Auto-updates enabled',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'unsafeMode',
|
|
||||||
label: (
|
|
||||||
<span>
|
|
||||||
Unsafe mode{' '}
|
|
||||||
<Badge danger fontSize={12}>
|
|
||||||
Dangerous
|
|
||||||
</Badge>
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
options: {
|
|
||||||
description: `Are you sure you want to turn this on?
|
|
||||||
You will be able to overwrite your system drives if you're not careful.`,
|
|
||||||
confirmLabel: 'Enable unsafe mode',
|
|
||||||
},
|
|
||||||
hide: await settings.get('disableUnsafeMode'),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
375
lib/gui/app/components/target-selector/target-selector-modal.tsx
Normal file
375
lib/gui/app/components/target-selector/target-selector-modal.tsx
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Drive as DrivelistDrive } from 'drivelist';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Badge, Table as BaseTable, Txt, Flex } from 'rendition';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import {
|
||||||
|
COMPATIBILITY_STATUS_TYPES,
|
||||||
|
getDriveImageCompatibilityStatuses,
|
||||||
|
hasListDriveImageCompatibilityStatus,
|
||||||
|
isDriveValid,
|
||||||
|
hasDriveImageCompatibilityStatus,
|
||||||
|
} from '../../../../shared/drive-constraints';
|
||||||
|
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||||
|
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||||
|
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||||
|
import { store } from '../../models/store';
|
||||||
|
import * as analytics from '../../modules/analytics';
|
||||||
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
|
import { Modal } from '../../styled-components';
|
||||||
|
|
||||||
|
export interface DrivelistTarget extends DrivelistDrive {
|
||||||
|
displayName: string;
|
||||||
|
progress: number;
|
||||||
|
device: string;
|
||||||
|
link: string;
|
||||||
|
linkTitle: string;
|
||||||
|
linkMessage: string;
|
||||||
|
linkCTA: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get a drive's compatibility status object(s)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Given a drive, return its compatibility status with the selected image,
|
||||||
|
* containing the status type (ERROR, WARNING), and accompanying
|
||||||
|
* status message.
|
||||||
|
*/
|
||||||
|
function getDriveStatuses(
|
||||||
|
drive: DrivelistTarget,
|
||||||
|
): Array<{ type: number; message: string }> {
|
||||||
|
return getDriveImageCompatibilityStatuses(drive, getImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
const TargetsTable = styled(({ refFn, ...props }) => {
|
||||||
|
return <BaseTable<DrivelistTarget> ref={refFn} {...props}></BaseTable>;
|
||||||
|
})`
|
||||||
|
[data-display='table-head']
|
||||||
|
[data-display='table-row']
|
||||||
|
> [data-display='table-cell']:first-child {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='table-head']
|
||||||
|
[data-display='table-row']
|
||||||
|
> [data-display='table-cell'] {
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #2a506f;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='table-body']
|
||||||
|
> [data-display='table-row']
|
||||||
|
> [data-display='table-cell']:first-child {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='table-body']
|
||||||
|
> [data-display='table-row']
|
||||||
|
> [data-display='table-cell'] {
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #2a506f;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface DriverlessDrive {
|
||||||
|
link: string;
|
||||||
|
linkTitle: string;
|
||||||
|
linkMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TargetStatus {
|
||||||
|
message: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatuses(statuses: TargetStatus[]) {
|
||||||
|
return _.map(statuses, (status) => {
|
||||||
|
const badgeShade =
|
||||||
|
status.type === COMPATIBILITY_STATUS_TYPES.WARNING ? 14 : 5;
|
||||||
|
return (
|
||||||
|
<Badge key={status.message} shade={badgeShade}>
|
||||||
|
{status.message}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const InitProgress = styled(
|
||||||
|
({
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
props?: React.ProgressHTMLAttributes<Element>;
|
||||||
|
}) => {
|
||||||
|
return <progress max="100" value={value} {...props}></progress>;
|
||||||
|
},
|
||||||
|
)`
|
||||||
|
/* Reset the default appearance */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
::-webkit-progress-bar {
|
||||||
|
width: 130px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #dde1f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-progress-value {
|
||||||
|
background-color: #1496e1;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function renderProgress(progress: number) {
|
||||||
|
if (Boolean(progress)) {
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<Txt fontSize={12}>Initializing device</Txt>
|
||||||
|
<InitProgress value={progress} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableData extends DrivelistTarget {
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TargetSelectorModal = styled(
|
||||||
|
({
|
||||||
|
close,
|
||||||
|
cancel,
|
||||||
|
}: {
|
||||||
|
close: (targets: DrivelistTarget[]) => void;
|
||||||
|
cancel: () => void;
|
||||||
|
}) => {
|
||||||
|
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||||
|
const [missingDriversModal, setMissingDriversModal] = React.useState(
|
||||||
|
defaultMissingDriversModalState,
|
||||||
|
);
|
||||||
|
const [drives, setDrives] = React.useState(getDrives());
|
||||||
|
const [selected, setSelected] = React.useState(getSelectedDrives());
|
||||||
|
const image = getImage();
|
||||||
|
|
||||||
|
const hasStatus = hasListDriveImageCompatibilityStatus(selected, image);
|
||||||
|
|
||||||
|
const tableData = _.map(drives, (drive) => {
|
||||||
|
return {
|
||||||
|
...drive,
|
||||||
|
extra: drive.progress || getDriveStatuses(drive),
|
||||||
|
disabled: !isDriveValid(drive, image) || drive.progress,
|
||||||
|
highlighted: hasDriveImageCompatibilityStatus(drive, image),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const disabledRows = _.map(
|
||||||
|
_.filter(drives, (drive) => {
|
||||||
|
return !isDriveValid(drive, image) || drive.progress;
|
||||||
|
}),
|
||||||
|
'displayName',
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'size',
|
||||||
|
label: 'Size',
|
||||||
|
render: (size: number) => {
|
||||||
|
return bytesToClosestUnit(size);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'link',
|
||||||
|
label: 'Location',
|
||||||
|
render: (link: string, drive: DrivelistTarget) => {
|
||||||
|
return !link ? (
|
||||||
|
<Txt>{drive.displayName}</Txt>
|
||||||
|
) : (
|
||||||
|
<Txt>
|
||||||
|
{drive.displayName} -{' '}
|
||||||
|
<b>
|
||||||
|
<a onClick={() => installMissingDrivers(drive)}>
|
||||||
|
{drive.linkCTA}
|
||||||
|
</a>
|
||||||
|
</b>
|
||||||
|
</Txt>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'extra',
|
||||||
|
label: ' ',
|
||||||
|
render: (extra: TargetStatus[] | number) => {
|
||||||
|
if (typeof extra === 'number') {
|
||||||
|
return renderProgress(extra);
|
||||||
|
}
|
||||||
|
return renderStatuses(extra);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const unsubscribe = store.subscribe(() => {
|
||||||
|
setDrives(getDrives());
|
||||||
|
setSelected(getSelectedDrives());
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Prompt the user to install missing usbboot drivers
|
||||||
|
*/
|
||||||
|
function installMissingDrivers(drive: {
|
||||||
|
link: string;
|
||||||
|
linkTitle: string;
|
||||||
|
linkMessage: string;
|
||||||
|
}) {
|
||||||
|
if (drive.link) {
|
||||||
|
analytics.logEvent('Open driver link modal', {
|
||||||
|
url: drive.link,
|
||||||
|
applicationSessionUuid: store.getState().toJS()
|
||||||
|
.applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
setMissingDriversModal({ drive });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
titleElement={
|
||||||
|
<Txt fontSize={24} align="left">
|
||||||
|
Select target
|
||||||
|
</Txt>
|
||||||
|
}
|
||||||
|
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||||
|
cancel={cancel}
|
||||||
|
done={() => close(selected)}
|
||||||
|
action="Continue"
|
||||||
|
style={{
|
||||||
|
width: '780px',
|
||||||
|
height: '420px',
|
||||||
|
}}
|
||||||
|
primaryButtonProps={{
|
||||||
|
primary: !hasStatus,
|
||||||
|
warning: hasStatus,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{!hasAvailableDrives() ? (
|
||||||
|
<div style={{ textAlign: 'center', margin: '0 auto' }}>
|
||||||
|
<b>Plug a target drive</b>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TargetsTable
|
||||||
|
refFn={(t: BaseTable<TableData>) => {
|
||||||
|
if (!_.isNull(t)) {
|
||||||
|
t.setRowSelection(selected);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
data={tableData}
|
||||||
|
disabledRows={disabledRows}
|
||||||
|
rowKey="displayName"
|
||||||
|
onCheck={(rows: TableData[]) => {
|
||||||
|
setSelected(rows);
|
||||||
|
}}
|
||||||
|
onRowClick={(row: TableData) => {
|
||||||
|
if (!row.disabled) {
|
||||||
|
const selectedIndex = selected.findIndex(
|
||||||
|
(target) => target.device === row.device,
|
||||||
|
);
|
||||||
|
if (selectedIndex === -1) {
|
||||||
|
selected.push(row);
|
||||||
|
setSelected(_.map(selected));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Deselect if selected
|
||||||
|
setSelected(
|
||||||
|
_.reject(
|
||||||
|
selected,
|
||||||
|
(drive) =>
|
||||||
|
selected[selectedIndex].device === drive.device,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></TargetsTable>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{missingDriversModal.drive !== undefined && (
|
||||||
|
<Modal
|
||||||
|
width={400}
|
||||||
|
title={missingDriversModal.drive.linkTitle}
|
||||||
|
cancel={() => setMissingDriversModal({})}
|
||||||
|
done={() => {
|
||||||
|
try {
|
||||||
|
if (missingDriversModal.drive !== undefined) {
|
||||||
|
openExternal(missingDriversModal.drive.link);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
analytics.logException(error);
|
||||||
|
} finally {
|
||||||
|
setMissingDriversModal({});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
action={'Yes, continue'}
|
||||||
|
cancelButtonProps={{
|
||||||
|
children: 'Cancel',
|
||||||
|
}}
|
||||||
|
children={
|
||||||
|
missingDriversModal.drive.linkMessage ||
|
||||||
|
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
||||||
|
}
|
||||||
|
></Modal>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)`
|
||||||
|
> [data-display='table-head']
|
||||||
|
> [data-display='table-row']
|
||||||
|
> [data-display='table-cell']:first-child {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
> [data-display='table-head']
|
||||||
|
> [data-display='table-row']
|
||||||
|
> [data-display='table-cell'] {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> [data-display='table-body']
|
||||||
|
> [data-display='table-row']
|
||||||
|
> [data-display='table-cell']:first-child {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
> [data-display='table-body']
|
||||||
|
> [data-display='table-row']
|
||||||
|
> [data-display='table-cell'] {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
`;
|
@ -17,9 +17,17 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
|
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||||
import { TargetSelector } from '../../components/drive-selector/target-selector';
|
import {
|
||||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
DrivelistTarget,
|
||||||
|
TargetSelectorModal,
|
||||||
|
} from '../../components/target-selector/target-selector-modal';
|
||||||
|
import {
|
||||||
|
getImage,
|
||||||
|
getSelectedDrives,
|
||||||
|
deselectDrive,
|
||||||
|
selectDrive,
|
||||||
|
} from '../../models/selection-state';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
import { observe } from '../../models/store';
|
import { observe } from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
@ -84,7 +92,7 @@ export const DriveSelector = ({
|
|||||||
{ showDrivesButton, driveListLabel, targets, image },
|
{ showDrivesButton, driveListLabel, targets, image },
|
||||||
setStateSlice,
|
setStateSlice,
|
||||||
] = React.useState(getDriveSelectionStateSlice());
|
] = React.useState(getDriveSelectionStateSlice());
|
||||||
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -115,11 +123,11 @@ export const DriveSelector = ({
|
|||||||
show={!hasDrive && showDrivesButton}
|
show={!hasDrive && showDrivesButton}
|
||||||
tooltip={driveListLabel}
|
tooltip={driveListLabel}
|
||||||
openDriveSelector={() => {
|
openDriveSelector={() => {
|
||||||
setShowDriveSelectorModal(true);
|
setShowTargetSelectorModal(true);
|
||||||
}}
|
}}
|
||||||
reselectDrive={() => {
|
reselectDrive={() => {
|
||||||
analytics.logEvent('Reselect drive');
|
analytics.logEvent('Reselect drive');
|
||||||
setShowDriveSelectorModal(true);
|
setShowTargetSelectorModal(true);
|
||||||
}}
|
}}
|
||||||
flashing={flashing}
|
flashing={flashing}
|
||||||
targets={targets}
|
targets={targets}
|
||||||
@ -127,10 +135,25 @@ export const DriveSelector = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showDriveSelectorModal && (
|
{showTargetSelectorModal && (
|
||||||
<DriveSelectorModal
|
<TargetSelectorModal
|
||||||
close={() => setShowDriveSelectorModal(false)}
|
cancel={() => setShowTargetSelectorModal(false)}
|
||||||
></DriveSelectorModal>
|
close={(selectedTargets: DrivelistTarget[]) => {
|
||||||
|
const selectedDrives = getSelectedDrives();
|
||||||
|
if (_.isEmpty(selectedTargets)) {
|
||||||
|
_.each(_.map(selectedDrives, 'device'), deselectDrive);
|
||||||
|
} else {
|
||||||
|
const deselected = _.reject(selectedDrives, (drive) =>
|
||||||
|
_.find(selectedTargets, (row) => row.device === drive.device),
|
||||||
|
);
|
||||||
|
// select drives
|
||||||
|
_.each(_.map(selectedTargets, 'device'), selectDrive);
|
||||||
|
// deselect drives
|
||||||
|
_.each(_.map(deselected, 'device'), deselectDrive);
|
||||||
|
}
|
||||||
|
setShowTargetSelectorModal(false);
|
||||||
|
}}
|
||||||
|
></TargetSelectorModal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -21,9 +21,12 @@ import { Flex, Modal, Txt } from 'rendition';
|
|||||||
|
|
||||||
import * as constraints from '../../../../shared/drive-constraints';
|
import * as constraints from '../../../../shared/drive-constraints';
|
||||||
import * as messages from '../../../../shared/messages';
|
import * as messages from '../../../../shared/messages';
|
||||||
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
|
|
||||||
import { ProgressButton } from '../../components/progress-button/progress-button';
|
import { ProgressButton } from '../../components/progress-button/progress-button';
|
||||||
import { SourceOptions } from '../../components/source-selector/source-selector';
|
import { SourceOptions } from '../../components/source-selector/source-selector';
|
||||||
|
import {
|
||||||
|
TargetSelectorModal,
|
||||||
|
DrivelistTarget,
|
||||||
|
} from '../../components/target-selector/target-selector-modal';
|
||||||
import * as availableDrives from '../../models/available-drives';
|
import * as availableDrives from '../../models/available-drives';
|
||||||
import * as flashState from '../../models/flash-state';
|
import * as flashState from '../../models/flash-state';
|
||||||
import * as selection from '../../models/selection-state';
|
import * as selection from '../../models/selection-state';
|
||||||
@ -325,11 +328,28 @@ export class FlashStep extends React.PureComponent<
|
|||||||
</Txt>
|
</Txt>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.state.showDriveSelectorModal && (
|
{this.state.showDriveSelectorModal && (
|
||||||
<DriveSelectorModal
|
<TargetSelectorModal
|
||||||
close={() => this.setState({ showDriveSelectorModal: false })}
|
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
||||||
/>
|
close={(targets: DrivelistTarget[]) => {
|
||||||
|
const selectedDrives = selection.getSelectedDrives();
|
||||||
|
if (_.isEmpty(targets)) {
|
||||||
|
_.each(
|
||||||
|
_.map(selectedDrives, 'device'),
|
||||||
|
selection.deselectDrive,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const deselected = _.reject(selectedDrives, (drive) =>
|
||||||
|
_.find(targets, (row) => row.device === drive.device),
|
||||||
|
);
|
||||||
|
// select drives
|
||||||
|
_.each(_.map(targets, 'device'), selection.selectDrive);
|
||||||
|
// deselect drives
|
||||||
|
_.each(_.map(deselected, 'device'), selection.deselectDrive);
|
||||||
|
}
|
||||||
|
this.setState({ showDriveSelectorModal: false });
|
||||||
|
}}
|
||||||
|
></TargetSelectorModal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -29,7 +29,6 @@ $disabled-opacity: 0.2;
|
|||||||
@import "./modules/space";
|
@import "./modules/space";
|
||||||
@import "./components/label";
|
@import "./components/label";
|
||||||
@import "./components/tick";
|
@import "./components/tick";
|
||||||
@import "../components/drive-selector/styles/drive-selector";
|
|
||||||
@import "../pages/main/styles/main";
|
@import "../pages/main/styles/main";
|
||||||
@import "../pages/finish/styles/finish";
|
@import "../pages/finish/styles/finish";
|
||||||
@import "./desktop";
|
@import "./desktop";
|
||||||
|
@ -25,3 +25,20 @@ html {
|
|||||||
body {
|
body {
|
||||||
background-color: $palette-theme-dark-background;
|
background-color: $palette-theme-dark-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix slight checkbox vertical alignment issue
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[uib-tooltip] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
@ -15,56 +15,27 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Button, ButtonProps, Provider, Txt } from 'rendition';
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonProps,
|
||||||
|
Flex,
|
||||||
|
Modal as ModalBase,
|
||||||
|
Provider,
|
||||||
|
Txt,
|
||||||
|
} from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { space } from 'styled-system';
|
import { space } from 'styled-system';
|
||||||
|
|
||||||
import { colors } from './theme';
|
import { colors, theme } from './theme';
|
||||||
|
|
||||||
const font = 'SourceSansPro';
|
|
||||||
const theme = {
|
|
||||||
font,
|
|
||||||
titleFont: font,
|
|
||||||
global: {
|
|
||||||
font: {
|
|
||||||
family: font,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colors,
|
|
||||||
button: {
|
|
||||||
border: {
|
|
||||||
width: '0',
|
|
||||||
radius: '24px',
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
extend: () => `
|
|
||||||
&& {
|
|
||||||
width: 200px;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: ${colors.dark.disabled.background};
|
|
||||||
color: ${colors.dark.disabled.foreground};
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: ${colors.dark.disabled.background};
|
|
||||||
color: ${colors.dark.disabled.foreground};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemedProvider = (props: any) => (
|
export const ThemedProvider = (props: any) => (
|
||||||
<Provider theme={theme} {...props}></Provider>
|
<Provider theme={theme} {...props}></Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BaseButton = styled(Button)`
|
export const BaseButton = styled(Button)`
|
||||||
|
width: 200px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const IconButton = styled((props) => <Button plain {...props} />)`
|
export const IconButton = styled((props) => <Button plain {...props} />)`
|
||||||
@ -81,7 +52,7 @@ export const IconButton = styled((props) => <Button plain {...props} />)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const StepButton = styled((props: ButtonProps) => (
|
export const StepButton = styled((props: ButtonProps) => (
|
||||||
<Button {...props}></Button>
|
<BaseButton {...props}></BaseButton>
|
||||||
))`
|
))`
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@ -105,10 +76,9 @@ export const ChangeButton = styled(Button)`
|
|||||||
${space}
|
${space}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export const StepNameButton = styled(Button)`
|
|
||||||
border-radius: 24px;
|
export const StepNameButton = styled(BaseButton)`
|
||||||
margin: auto;
|
display: inline-flex;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -123,16 +93,52 @@ export const StepNameButton = styled(Button)`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const StepSelection = styled(Flex)`
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
export const Footer = styled(Txt)`
|
export const Footer = styled(Txt)`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: ${colors.dark.disabled.foreground};
|
color: ${colors.dark.disabled.foreground};
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Underline = styled(Txt.span)`
|
export const Underline = styled(Txt.span)`
|
||||||
border-bottom: 1px dotted;
|
border-bottom: 1px dotted;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DetailsText = styled(Txt.p)`
|
export const DetailsText = styled(Txt.p)`
|
||||||
color: ${colors.dark.disabled.foreground};
|
color: ${colors.dark.disabled.foreground};
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Modal = styled((props) => {
|
||||||
|
return (
|
||||||
|
<ModalBase
|
||||||
|
cancelButtonProps={{
|
||||||
|
style: {
|
||||||
|
marginRight: '20px',
|
||||||
|
border: 'solid 1px #2a506f',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})`
|
||||||
|
> div {
|
||||||
|
padding: 30px;
|
||||||
|
|
||||||
|
> div:last-child {
|
||||||
|
height: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 -2px 10px 0 rgba(221, 225, 240, 0.5), 0 -1px 0 0 #dde1f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const font = 'SourceSansPro';
|
||||||
|
|
||||||
export const colors = {
|
export const colors = {
|
||||||
dark: {
|
dark: {
|
||||||
foreground: '#fff',
|
foreground: '#fff',
|
||||||
@ -64,3 +66,40 @@ export const colors = {
|
|||||||
background: '#5fb835',
|
background: '#5fb835',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const theme = {
|
||||||
|
font,
|
||||||
|
titleFont: font,
|
||||||
|
global: {
|
||||||
|
font: {
|
||||||
|
family: font,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors,
|
||||||
|
button: {
|
||||||
|
border: {
|
||||||
|
width: '0',
|
||||||
|
radius: '24px',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
extend: () => `
|
||||||
|
&& {
|
||||||
|
width: 200px;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
background-color: ${colors.dark.disabled.background};
|
||||||
|
color: ${colors.dark.disabled.foreground};
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
background-color: ${colors.dark.disabled.background};
|
||||||
|
color: ${colors.dark.disabled.foreground};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user