diff --git a/src/components/ModalAddressMode.js b/src/components/ModalAddressMode.js index 191cd200..424ffa10 100644 --- a/src/components/ModalAddressMode.js +++ b/src/components/ModalAddressMode.js @@ -11,6 +11,8 @@ import { t } from 'ttag'; import { getGlobalWallet } from '../modules/wallet'; import helpers from '../utils/helpers'; import { ADDRESS_MODE } from '../constants'; +import RadioGroup from './Radio/RadioGroup'; +import PreferenceSaveButton from './PreferenceSaveButton'; const LEARN_MORE_URL = 'https://docs.hathor.network/explanations/features/wallet-service/address-mode'; @@ -66,78 +68,53 @@ function ModalAddressMode({ currentMode, onSave, onClose }) { -
-

+

+

{t`You can set your wallet to `}{t`single or multi address mode`}{'.'}
{t`You can switch anytime in the settings.`}

-
!isSingleDisabled && setSelectedMode(ADDRESS_MODE.SINGLE)} - > -
- setSelectedMode(ADDRESS_MODE.SINGLE)} - /> - {t`Single Address`} -
-

- {t`Your wallet will use only one address (index 0).`} -
- {t`Easier to manage and compatible with all dApps.`} -

-
+ -
-
setSelectedMode(ADDRESS_MODE.MULTI)} - > -
- setSelectedMode(ADDRESS_MODE.MULTI)} - /> - {t`Multi Address`} -
-

- {t`Your wallet will let you generate multiple addresses.`} -
- {t`Useful for advanced users.`} -

+ {hasTxOutside && ( +
+ + + {t`You can't switch to single address mode because other addresses in your wallet are already in use.`} + {' '} + {t`Learn more`}{'.'} +
- - {hasTxOutside && ( -
- - - {t`You can't switch to single address mode because other addresses in your wallet are already in use.`} - {' '} - {t`Learn more`}{'.'} - -
- )} -
+ )}
-
- + />
diff --git a/src/components/ModalAmountFormat.js b/src/components/ModalAmountFormat.js index 7330d66e..7346bd57 100644 --- a/src/components/ModalAmountFormat.js +++ b/src/components/ModalAmountFormat.js @@ -10,6 +10,8 @@ import $ from 'jquery'; import { t } from 'ttag'; import { AMOUNT_FORMAT } from '../constants'; import { compressAmountString } from '../utils/amount'; +import RadioGroup from './Radio/RadioGroup'; +import PreferenceSaveButton from './PreferenceSaveButton'; const MODAL_ID = 'amountFormatModal'; @@ -40,27 +42,6 @@ function ModalAmountFormat({ currentFormat, onSave, onClose }) { return `${PREVIEW_EXPANDED} HTR`; }; - const renderOption = ({ format, title, description, isDefault }) => ( -
setSelectedFormat(format)}> - setSelectedFormat(format)} - /> -
-
- - {isDefault && {t`Default`}} -
-

{description}

-
-
- ); - return ( diff --git a/src/components/PreferenceSaveButton.js b/src/components/PreferenceSaveButton.js new file mode 100644 index 00000000..eadb0725 --- /dev/null +++ b/src/components/PreferenceSaveButton.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * The primary save button shared by the preference modals. Renders only the + * button; the caller supplies its own container and spacing. + * + * @memberof Components + */ +function PreferenceSaveButton({ title, onClick, disabled }) { + return ( + + ); +} + +PreferenceSaveButton.propTypes = { + title: PropTypes.node.isRequired, + onClick: PropTypes.func.isRequired, + disabled: PropTypes.bool, +}; + +PreferenceSaveButton.defaultProps = { + disabled: false, +}; + +export default PreferenceSaveButton; diff --git a/src/components/Radio/RadioButton.js b/src/components/Radio/RadioButton.js new file mode 100644 index 00000000..3e1ece89 --- /dev/null +++ b/src/components/Radio/RadioButton.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * The radio circle itself. A real input so keyboard and assistive tech work; + * `appearance: none` in SCSS replaces the native rendering. + * + * @memberof Components + */ +function RadioButton({ id, name, selected, disabled, onChange }) { + return ( + + ); +} + +RadioButton.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + selected: PropTypes.bool.isRequired, + disabled: PropTypes.bool, + onChange: PropTypes.func.isRequired, +}; + +RadioButton.defaultProps = { + disabled: false, +}; + +export default RadioButton; diff --git a/src/components/Radio/RadioGroup.js b/src/components/Radio/RadioGroup.js new file mode 100644 index 00000000..650bb701 --- /dev/null +++ b/src/components/Radio/RadioGroup.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import RadioOption from './RadioOption'; + +/** + * A bordered card grouping radio options, separated by dividers. Controlled: + * the caller owns the selected value and receives it back through onChange. + * + * @memberof Components + */ +function RadioGroup({ name, value, onChange, options }) { + return ( +
+ {options.map((option, index) => ( + + {index > 0 &&
} + +
+ ))} +
+ ); +} + +RadioGroup.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, + description: PropTypes.node, + hint: PropTypes.node, + badge: PropTypes.node, + disabled: PropTypes.bool, + })).isRequired, +}; + +export default RadioGroup; diff --git a/src/components/Radio/RadioOption.js b/src/components/Radio/RadioOption.js new file mode 100644 index 00000000..42c64a28 --- /dev/null +++ b/src/components/Radio/RadioOption.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import RadioButton from './RadioButton'; + +/** + * A selectable row: radio circle, a title row carrying an optional badge pill, + * an optional description and an optional muted italic hint. + * + * @memberof Components + */ +function RadioOption({ name, value, selected, disabled, onSelect, title, badge, description, hint }) { + const inputId = `${name}-${value}`; + const handleSelect = () => { + if (!disabled) { + onSelect(value); + } + }; + + const renderBadge = () => { + if (!badge) { + return null; + } + return {badge}; + }; + + const renderDescription = () => { + if (!description) { + return null; + } + return

{description}

; + }; + + const renderHint = () => { + if (!hint) { + return null; + } + return {hint}; + }; + + return ( +
+ +
+
+ + {renderBadge()} +
+ {renderDescription()} + {renderHint()} +
+
+ ); +} + +RadioOption.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + selected: PropTypes.bool.isRequired, + disabled: PropTypes.bool, + onSelect: PropTypes.func.isRequired, + title: PropTypes.node.isRequired, + badge: PropTypes.node, + description: PropTypes.node, + hint: PropTypes.node, +}; + +RadioOption.defaultProps = { + disabled: false, + badge: null, + description: null, + hint: null, +}; + +export default RadioOption; diff --git a/src/components/Radio/index.js b/src/components/Radio/index.js new file mode 100644 index 00000000..c6a8e96f --- /dev/null +++ b/src/components/Radio/index.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export { default as RadioButton } from './RadioButton'; +export { default as RadioOption } from './RadioOption'; +export { default as RadioGroup } from './RadioGroup'; diff --git a/src/index.module.scss b/src/index.module.scss index 71846993..2f0ab59e 100644 --- a/src/index.module.scss +++ b/src/index.module.scss @@ -1210,82 +1210,155 @@ div .nc-token-balance:not(:last-child) { } } -/* Address Mode modal */ -.address-mode { - &-option { - background: #f7f7f7; - border-radius: 16px; - padding: 24px 16px; - margin-bottom: 16px; - - input[type="radio"] { - appearance: none; - -webkit-appearance: none; - width: 24px; - height: 24px; - border: 2px solid #c4c4c4; - border-radius: 50%; - margin: 0; - cursor: pointer; - position: relative; - flex-shrink: 0; - - &:checked { - border-color: $purpleHathor; - - &::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 12px; - height: 12px; - border-radius: 50%; - background: $purpleHathor; - } - } - } +/* Shared radio selector, used by the preference modals */ +.radio-group { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 16px; + padding: 24px 16px; - &--disabled { - input[type="radio"] { - border-color: #e0e0e0; - cursor: default; - } + &-divider { + margin: 24px 0; + border-top: 1px solid #e5e7eb; + } +} - .address-mode-label { - color: #c4c4c4; - cursor: default; - } +.radio-option { + display: flex; + align-items: flex-start; + gap: 16px; + cursor: pointer; - .address-mode-description { - color: #b0b0b0; - } + &--disabled { + cursor: default; - .address-mode-hint { - color: #b0b0b0; - font-style: normal; - } + .radio-option-title { + color: #c4c4c4; + cursor: default; + } + + .radio-option-description, + .radio-option-hint { + color: #b0b0b0; + } + + .radio-option-hint { + font-style: normal; } } - &-label { - color: $purpleHathor; - font-size: 14px; + &-body { + flex: 1; + min-width: 0; + } + + &-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + } + + &-title { + font-size: 16px; + font-weight: 600; + line-height: 20px; + color: #000; + margin-bottom: 4px; cursor: pointer; } &-description { font-size: 12px; - margin-bottom: 0; line-height: 20px; + color: #979797; + margin-bottom: 0; } &-hint { + font-size: 12px; + line-height: 20px; font-style: italic; color: #57606a; } + &-badge { + background: #f2f3f5; + border-radius: 8px; + width: 69px; + height: 23px; + font-size: 12px; + line-height: 20px; + color: #6b7280; + text-align: center; + flex-shrink: 0; + padding: 2px 0; + } +} + +.radio-button { + appearance: none; + -webkit-appearance: none; + width: 26px; + height: 26px; + border: 2px solid #c4c4c4; + border-radius: 50%; + margin: 0; + cursor: pointer; + position: relative; + flex-shrink: 0; + + &:checked { + border-color: $purpleHathor; + + &::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 13px; + height: 13px; + border-radius: 50%; + background: $purpleHathor; + } + } + + &:disabled { + border-color: #e0e0e0; + cursor: default; + } +} + +.preference-save-btn { + background: #e5e5e5; + border: none; + border-radius: 8px; + padding: 16px; + width: 226px; + color: #b0b0b0; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + cursor: default; + + &--active { + background: $purpleHathor; + color: white; + cursor: pointer; + + &:hover { + background: $purpleHathorHover; + } + } +} + +/* Address Mode modal */ +.address-mode { + &-intro { + margin-bottom: 40px; + } + &-alert { background: rgba(217, 119, 6, 0.2); border-radius: 8px; @@ -1297,26 +1370,8 @@ div .nc-token-balance:not(:last-child) { line-height: 20px; } - &-save-btn { - background: #e5e5e5; - border: none; - border-radius: 8px; - padding: 16px; - width: 185px; - color: #b0b0b0; - font-weight: 600; - font-size: 14px; - cursor: default; - - &--active { - background: $purpleHathor; - color: white; - cursor: pointer; - - &:hover { - background: $purpleHathorHover; - } - } + &-footer { + margin-top: 12px; } } @@ -1341,95 +1396,6 @@ div .nc-token-balance:not(:last-child) { margin-bottom: 24px; } - &-card { - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 16px; - padding: 24px 16px; - } - - &-divider { - margin: 24px 0; - border-top: 1px solid #e5e7eb; - } - - &-option { - display: flex; - align-items: flex-start; - gap: 16px; - cursor: pointer; - - input[type=radio] { - appearance: none; - -webkit-appearance: none; - width: 26px; - height: 26px; - border: 2px solid #c4c4c4; - border-radius: 50%; - margin: 0; - cursor: pointer; - position: relative; - flex-shrink: 0; - - &:checked { - border-color: $purpleHathor; - - &::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 13px; - height: 13px; - border-radius: 50%; - background: $purpleHathor; - } - } - } - - &-body { - flex: 1; - min-width: 0; - } - - &-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 8px; - } - - &-title { - font-size: 16px; - font-weight: 600; - line-height: 20px; - color: #000; - margin-bottom: 4px; - cursor: pointer; - } - - &-description { - font-size: 12px; - line-height: 20px; - color: #979797; - margin-bottom: 0; - } - } - - &-tag { - background: #f2f3f5; - border-radius: 8px; - width: 69px; - height: 23px; - font-size: 12px; - line-height: 20px; - color: #6b7280; - text-align: center; - flex-shrink: 0; - padding: 2px 0; - } - &-preview-label { font-size: 14px; font-weight: 600; @@ -1463,27 +1429,4 @@ div .nc-token-balance:not(:last-child) { overflow-wrap: anywhere; } } - - &-save-btn { - background: #e5e5e5; - border: none; - border-radius: 8px; - padding: 16px; - width: 226px; - color: #b0b0b0; - font-weight: 600; - font-size: 14px; - text-transform: uppercase; - cursor: default; - - &--active { - background: $purpleHathor; - color: white; - cursor: pointer; - - &:hover { - background: $purpleHathorHover; - } - } - } }