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:
Lorenzo Alberto Maria Ambrosi 2020-03-13 10:28:56 +01:00
parent f8cc7c36b4
commit 71c7fbd3a2
11 changed files with 541 additions and 472 deletions

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -19,7 +19,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash';
import * as os from 'os';
import * as React from 'react';
import { Badge, Checkbox, Modal } from 'rendition';
import { Checkbox, Modal } from 'rendition';
import { version } from '../../../../../package.json';
import * as settings from '../../models/settings';
@ -92,23 +92,6 @@ async function getSettingsList(): Promise<Setting[]> {
name: 'updatesEnabled',
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'),
},
];
}

View 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;
}
`;

View File

@ -17,9 +17,17 @@
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { TargetSelector } from '../../components/drive-selector/target-selector';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import { TargetSelector } from '../../components/target-selector/target-selector-button';
import {
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 { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
@ -84,7 +92,7 @@ export const DriveSelector = ({
{ showDrivesButton, driveListLabel, targets, image },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
false,
);
@ -115,11 +123,11 @@ export const DriveSelector = ({
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowDriveSelectorModal(true);
setShowTargetSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowDriveSelectorModal(true);
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
@ -127,10 +135,25 @@ export const DriveSelector = ({
/>
</div>
{showDriveSelectorModal && (
<DriveSelectorModal
close={() => setShowDriveSelectorModal(false)}
></DriveSelectorModal>
{showTargetSelectorModal && (
<TargetSelectorModal
cancel={() => setShowTargetSelectorModal(false)}
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>
);

View File

@ -21,9 +21,12 @@ import { Flex, Modal, Txt } from 'rendition';
import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { ProgressButton } from '../../components/progress-button/progress-button';
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 flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state';
@ -325,11 +328,28 @@ export class FlashStep extends React.PureComponent<
</Txt>
</Modal>
)}
{this.state.showDriveSelectorModal && (
<DriveSelectorModal
close={() => this.setState({ showDriveSelectorModal: false })}
/>
<TargetSelectorModal
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>
)}
</>
);

View File

@ -29,7 +29,6 @@ $disabled-opacity: 0.2;
@import "./modules/space";
@import "./components/label";
@import "./components/tick";
@import "../components/drive-selector/styles/drive-selector";
@import "../pages/main/styles/main";
@import "../pages/finish/styles/finish";
@import "./desktop";

View File

@ -25,3 +25,20 @@ html {
body {
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;
}

View File

@ -15,56 +15,27 @@
*/
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 { space } from 'styled-system';
import { colors } 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};
}
}
}
`,
},
};
import { colors, theme } from './theme';
export const ThemedProvider = (props: any) => (
<Provider theme={theme} {...props}></Provider>
);
export const BaseButton = styled(Button)`
width: 200px;
height: 48px;
font-size: 16px;
`;
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) => (
<Button {...props}></Button>
<BaseButton {...props}></BaseButton>
))`
color: #ffffff;
margin: auto;
@ -105,10 +76,9 @@ export const ChangeButton = styled(Button)`
${space}
}
`;
export const StepNameButton = styled(Button)`
border-radius: 24px;
margin: auto;
display: flex;
export const StepNameButton = styled(BaseButton)`
display: inline-flex;
justify-content: center;
align-items: center;
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)`
margin-top: 10px;
color: ${colors.dark.disabled.foreground};
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 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;
}
}
`;

View File

@ -14,6 +14,8 @@
* limitations under the License.
*/
const font = 'SourceSansPro';
export const colors = {
dark: {
foreground: '#fff',
@ -64,3 +66,40 @@ export const colors = {
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};
}
}
}
`,
},
};