diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx deleted file mode 100644 index 477fb439..00000000 --- a/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx +++ /dev/null @@ -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, -) { - 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 ( - - - - {missingDriversModal.drive !== undefined && ( - 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` - } - > - )} - - ); -} diff --git a/lib/gui/app/components/drive-selector/styles/_drive-selector.scss b/lib/gui/app/components/drive-selector/styles/_drive-selector.scss deleted file mode 100644 index 809f693f..00000000 --- a/lib/gui/app/components/drive-selector/styles/_drive-selector.scss +++ /dev/null @@ -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; - } -} - diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index 1811d0e5..9b1bf1a2 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -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 { name: 'updatesEnabled', label: 'Auto-updates enabled', }, - { - name: 'unsafeMode', - label: ( - - Unsafe mode{' '} - - Dangerous - - - ), - 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'), - }, ]; } diff --git a/lib/gui/app/components/drive-selector/target-selector.tsx b/lib/gui/app/components/target-selector/target-selector-button.tsx similarity index 100% rename from lib/gui/app/components/drive-selector/target-selector.tsx rename to lib/gui/app/components/target-selector/target-selector-button.tsx diff --git a/lib/gui/app/components/target-selector/target-selector-modal.tsx b/lib/gui/app/components/target-selector/target-selector-modal.tsx new file mode 100644 index 00000000..7fbe24c1 --- /dev/null +++ b/lib/gui/app/components/target-selector/target-selector-modal.tsx @@ -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 ref={refFn} {...props}>; +})` + [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 ( + + {status.message} + + ); + }); +} + +const InitProgress = styled( + ({ + value, + ...props + }: { + value: number; + props?: React.ProgressHTMLAttributes; + }) => { + return ; + }, +)` + /* 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 ( + + Initializing device + + + ); + } + 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 ? ( + {drive.displayName} + ) : ( + + {drive.displayName} -{' '} + + installMissingDrivers(drive)}> + {drive.linkCTA} + + + + ); + }, + }, + { + 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 ( + + Select target + + } + titleDetails={{getDrives().length} found} + cancel={cancel} + done={() => close(selected)} + action="Continue" + style={{ + width: '780px', + height: '420px', + }} + primaryButtonProps={{ + primary: !hasStatus, + warning: hasStatus, + }} + > +
+ {!hasAvailableDrives() ? ( +
+ Plug a target drive +
+ ) : ( + ) => { + 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, + ), + ); + } + }} + > + )} +
+ + {missingDriversModal.drive !== undefined && ( + 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` + } + > + )} +
+ ); + }, +)` + > [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; + } +`; diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 1cac4f82..2bf9bc00 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -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 = ({ /> - {showDriveSelectorModal && ( - setShowDriveSelectorModal(false)} - > + {showTargetSelectorModal && ( + 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); + }} + > )} ); diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index a2fd2690..0c236491 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -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< )} - {this.state.showDriveSelectorModal && ( - this.setState({ showDriveSelectorModal: false })} - /> + 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 }); + }} + > )} ); diff --git a/lib/gui/app/scss/main.scss b/lib/gui/app/scss/main.scss index 972a34f6..b111f354 100644 --- a/lib/gui/app/scss/main.scss +++ b/lib/gui/app/scss/main.scss @@ -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"; diff --git a/lib/gui/app/scss/modules/_bootstrap.scss b/lib/gui/app/scss/modules/_bootstrap.scss index b49adddb..3ad54144 100644 --- a/lib/gui/app/scss/modules/_bootstrap.scss +++ b/lib/gui/app/scss/modules/_bootstrap.scss @@ -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; +} diff --git a/lib/gui/app/styled-components.tsx b/lib/gui/app/styled-components.tsx index fd7ddd09..d7d91af0 100644 --- a/lib/gui/app/styled-components.tsx +++ b/lib/gui/app/styled-components.tsx @@ -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) => ( ); export const BaseButton = styled(Button)` + width: 200px; height: 48px; + font-size: 16px; `; export const IconButton = styled((props) => + ))` 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 ( + + ); +})` + > 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; + } + } +`; diff --git a/lib/gui/app/theme.ts b/lib/gui/app/theme.ts index c12d22b4..a2184782 100644 --- a/lib/gui/app/theme.ts +++ b/lib/gui/app/theme.ts @@ -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}; + } + } + } + `, + }, +};