From b2113da2ed9d5805f2c60f1a7be121e9ac6bd6ff Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 8 Sep 2023 12:38:28 +0100 Subject: [PATCH 01/10] Add name and combo components --- .circleci/config.yml | 3 - .github/workflows/codeql-analysis.yml | 6 +- .yalc/@metamask/name-controller/LICENSE | 20 + .yalc/@metamask/name-controller/README.md | 15 + .yalc/@metamask/name-controller/package.json | 43 +++ .yalc/@metamask/name-controller/yalc.sig | 1 + app/_locales/en/messages.json | 36 ++ package.json | 1 + ui/components/app/app-components.scss | 3 + .../form-combo-field.test.tsx.snap | 133 +++++++ .../form-combo-field.stories.tsx | 103 +++++ .../form-combo-field.test.tsx | 109 ++++++ .../app/form-combo-field/form-combo-field.tsx | 222 +++++++++++ ui/components/app/form-combo-field/index.scss | 58 +++ ui/components/app/form-combo-field/index.ts | 1 + .../app/name/__snapshots__/name.test.tsx.snap | 95 +++++ ui/components/app/name/index.scss | 36 ++ ui/components/app/name/index.ts | 1 + .../__snapshots__/name-details.test.tsx.snap | 365 ++++++++++++++++++ .../app/name/name-details/index.scss | 15 + ui/components/app/name/name-details/index.ts | 1 + .../name/name-details/name-details.test.tsx | 220 +++++++++++ .../app/name/name-details/name-details.tsx | 203 ++++++++++ ui/components/app/name/name.stories.tsx | 151 ++++++++ ui/components/app/name/name.test.tsx | 107 +++++ ui/components/app/name/name.tsx | 74 ++++ ui/hooks/useName.test.ts | 145 +++++++ ui/hooks/useName.ts | 25 ++ ui/selectors/selectors.js | 13 + ui/store/actions.ts | 26 ++ yalc.lock | 9 + yarn.lock | 11 + 32 files changed, 2245 insertions(+), 6 deletions(-) create mode 100644 .yalc/@metamask/name-controller/LICENSE create mode 100644 .yalc/@metamask/name-controller/README.md create mode 100644 .yalc/@metamask/name-controller/package.json create mode 100644 .yalc/@metamask/name-controller/yalc.sig create mode 100644 ui/components/app/form-combo-field/__snapshots__/form-combo-field.test.tsx.snap create mode 100644 ui/components/app/form-combo-field/form-combo-field.stories.tsx create mode 100644 ui/components/app/form-combo-field/form-combo-field.test.tsx create mode 100644 ui/components/app/form-combo-field/form-combo-field.tsx create mode 100644 ui/components/app/form-combo-field/index.scss create mode 100644 ui/components/app/form-combo-field/index.ts create mode 100644 ui/components/app/name/__snapshots__/name.test.tsx.snap create mode 100644 ui/components/app/name/index.scss create mode 100644 ui/components/app/name/index.ts create mode 100644 ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap create mode 100644 ui/components/app/name/name-details/index.scss create mode 100644 ui/components/app/name/name-details/index.ts create mode 100644 ui/components/app/name/name-details/name-details.test.tsx create mode 100644 ui/components/app/name/name-details/name-details.tsx create mode 100644 ui/components/app/name/name.stories.tsx create mode 100644 ui/components/app/name/name.test.tsx create mode 100644 ui/components/app/name/name.tsx create mode 100644 ui/hooks/useName.test.ts create mode 100644 ui/hooks/useName.ts create mode 100644 yalc.lock diff --git a/.circleci/config.yml b/.circleci/config.yml index 78528233631e..9ea2e9072cad 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1157,9 +1157,6 @@ jobs: - store_artifacts: path: builds-flask destination: builds-flask - - store_artifacts: - path: builds-mmi - destination: builds-mmi - store_artifacts: path: coverage destination: coverage diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e5c1eb06b588..f945eb7b7553 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v1 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v1 diff --git a/.yalc/@metamask/name-controller/LICENSE b/.yalc/@metamask/name-controller/LICENSE new file mode 100644 index 000000000000..b703d6a4a23f --- /dev/null +++ b/.yalc/@metamask/name-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/.yalc/@metamask/name-controller/README.md b/.yalc/@metamask/name-controller/README.md new file mode 100644 index 000000000000..79818296606d --- /dev/null +++ b/.yalc/@metamask/name-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/name-controller` + +Stores and suggests names for values such as Ethereum addresses. + +## Installation + +`yarn add @metamask/name-controller` + +or + +`npm install @metamask/name-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/.yalc/@metamask/name-controller/package.json b/.yalc/@metamask/name-controller/package.json new file mode 100644 index 000000000000..b40c1bc10610 --- /dev/null +++ b/.yalc/@metamask/name-controller/package.json @@ -0,0 +1,43 @@ +{ + "name": "@metamask/name-controller", + "version": "0.0.0", + "description": "Stores and suggests names for values such as Ethereum addresses", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/name-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/name-controller", + "prepare-manifest:preview": "../../scripts/prepare-preview-manifest.sh", + "publish:preview": "yarn npm publish --tag preview", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^3.2.1", + "immer": "^9.0.6" + }, + "engines": { + "node": ">=16.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "yalcSig": "0491b6ca68300d382d0c5df3391fa9ed" +} diff --git a/.yalc/@metamask/name-controller/yalc.sig b/.yalc/@metamask/name-controller/yalc.sig new file mode 100644 index 000000000000..0b9ea98f80bc --- /dev/null +++ b/.yalc/@metamask/name-controller/yalc.sig @@ -0,0 +1 @@ +0491b6ca68300d382d0c5df3391fa9ed \ No newline at end of file diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 0f4a987282dd..ab0b23f5b54c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -714,6 +714,10 @@ "coingecko": { "message": "CoinGecko" }, + "comboNoOptions": { + "message": "No options found", + "description": "Default text shown in the combo field dropdown if no options." + }, "configureSnapPopupDescription": { "message": "You're now leaving MetaMask to configure this snap." }, @@ -2365,6 +2369,38 @@ "name": { "message": "Name" }, + "nameAddressLabel": { + "message": "Address", + "description": "Label above address field in name component modal." + }, + "nameInstructionsNew": { + "message": "You are interacting with an unknown contract address. If you trust this author, set a personal display name to identify it going forward.", + "description": "Instruction text in name component modal when value is not recognised." + }, + "nameInstructionsSaved": { + "message": "Interactions with this address will always be identified using this personal display name.", + "description": "Instruction text in name component modal when value is saved." + }, + "nameLabel": { + "message": "Display name", + "description": "Label above name input field in name component modal." + }, + "nameModalTitleNew": { + "message": "Unknown address", + "description": "Title of the modal created by the name component when value is not recognised." + }, + "nameModalTitleSaved": { + "message": "Saved address", + "description": "Title of the modal created by the name component when value is saved." + }, + "nameNoProposedNames": { + "message": "No proposed names found", + "description": "Text shown in the proposed name dropdown if none found." + }, + "nameSetPlaceholder": { + "message": "Set a personal display name...", + "description": "Placeholder text for name input field in name component modal." + }, "nativeToken": { "message": "The native token on this network is $1. It is the token used for gas fees.", "description": "$1 represents the name of the native token on the current network" diff --git a/package.json b/package.json index f42149026df8..c704c4b00429 100644 --- a/package.json +++ b/package.json @@ -255,6 +255,7 @@ "@metamask/logo": "^3.1.1", "@metamask/message-manager": "^7.3.0", "@metamask/metamask-eth-abis": "^3.0.0", + "@metamask/name-controller": "file:.yalc/@metamask/name-controller", "@metamask/network-controller": "^12.0.0", "@metamask/notification-controller": "^3.0.0", "@metamask/obs-store": "^8.1.0", diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index c4dc26714527..dada73af75a5 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -95,6 +95,9 @@ @import 'network-account-balance-header/index'; @import 'approve-content-card/index'; @import 'transaction-alerts/transaction-alerts'; +@import 'name/index'; +@import 'name/name-details/index'; +@import 'form-combo-field/index'; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) @import '../institutional/interactive-replacement-token-notification/index'; @import '../institutional/confirm-remove-jwt-modal/index'; diff --git a/ui/components/app/form-combo-field/__snapshots__/form-combo-field.test.tsx.snap b/ui/components/app/form-combo-field/__snapshots__/form-combo-field.test.tsx.snap new file mode 100644 index 000000000000..b441c5862780 --- /dev/null +++ b/ui/components/app/form-combo-field/__snapshots__/form-combo-field.test.tsx.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormComboField renders with no options 1`] = ` + +
+
+
+
+
+
+ + +
+
+
+
+
+ + TestNoOptionsText + +
+
+
+
+ +`; + +exports[`FormComboField renders with options 1`] = ` + +
+
+
+
+
+
+ + +
+
+
+
+
+ + TestPrimaryLabel + + + TestSecondaryLabel + +
+
+ + TestPrimaryLabel2 + + + TestSecondaryLabel2 + +
+
+
+
+ +`; diff --git a/ui/components/app/form-combo-field/form-combo-field.stories.tsx b/ui/components/app/form-combo-field/form-combo-field.stories.tsx new file mode 100644 index 000000000000..69bd3a77464c --- /dev/null +++ b/ui/components/app/form-combo-field/form-combo-field.stories.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useState } from 'react'; +import FormComboField from './form-combo-field'; + +/** + * A form field that supports free text entry or the selection of a value from an attached dropdown list. + */ +// eslint-disable-next-line import/no-anonymous-default-export +export default { + title: 'Components/App/FormComboField', + component: FormComboField, + argTypes: { + value: { + control: 'text', + description: 'The value to display in the field.', + }, + options: { + control: 'object', + description: `The options to display in the dropdown.

+ Must be an array of objects with a \`primaryLabel\` and optionally a \`secondaryLabel\` property.`, + }, + placeholder: { + control: 'text', + description: + 'The placeholder text to display in the field when the value is empty.', + }, + noOptionsText: { + control: 'text', + description: `The text to display in the dropdown when there are no options to display.`, + table: { + defaultValue: { summary: 'No options found' }, + }, + }, + maxDropdownHeight: { + control: 'number', + description: 'The maximum height of the dropdown in pixels.', + table: { + defaultValue: { summary: 179 }, + }, + }, + onChange: { + description: `Optional callback function to invoke when the value changes.`, + }, + onOptionClick: { + description: `Optional callback function to invoke when a dropdown option is clicked.`, + }, + }, + args: { + value: undefined, + options: [ + { primaryLabel: 'Berlin', secondaryLabel: 'Germany' }, + { primaryLabel: 'London', secondaryLabel: 'United Kingdom' }, + { primaryLabel: 'Lisbon', secondaryLabel: 'Portugal' }, + { primaryLabel: 'Paris', secondaryLabel: 'France' }, + ], + placeholder: 'Specify a city...', + noOptionsText: undefined, + maxDropdownHeight: undefined, + onChange: undefined, + onOptionClick: undefined, + }, +}; + +export const DefaultStory = (args) => { + const [value, setValue] = useState(''); + + const handleChange = useCallback( + (newValue: string) => { + setValue(newValue); + }, + [setValue], + ); + + return ( +
+ +
+ ); +}; + +DefaultStory.storyName = 'With Options'; + +export const NoOptionsStory = () => { + const [value, setValue] = useState(''); + + const handleChange = useCallback( + (newValue: string) => { + setValue(newValue); + }, + [setValue], + ); + + return ( +
+ +
+ ); +}; diff --git a/ui/components/app/form-combo-field/form-combo-field.test.tsx b/ui/components/app/form-combo-field/form-combo-field.test.tsx new file mode 100644 index 000000000000..46ea515d9168 --- /dev/null +++ b/ui/components/app/form-combo-field/form-combo-field.test.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { act } from 'react-dom/test-utils'; +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import FormComboField from './form-combo-field'; + +const VALUE_MOCK = 'TestValue'; +const PLACEHOLDER_MOCK = 'TestPlaceholder'; +const NO_OPTIONS_TEXT_MOCK = 'TestNoOptionsText'; + +const OPTIONS_MOCK = [ + { primaryLabel: 'TestPrimaryLabel', secondaryLabel: 'TestSecondaryLabel' }, + { primaryLabel: 'TestPrimaryLabel2', secondaryLabel: 'TestSecondaryLabel2' }, +]; + +describe('FormComboField', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders with options', async () => { + const { baseElement, getByPlaceholderText } = renderWithProvider( + , + ); + + const input = getByPlaceholderText(PLACEHOLDER_MOCK); + + await act(async () => { + fireEvent.click(input); + }); + + expect(baseElement).toMatchSnapshot(); + }); + + it('renders with no options', async () => { + const { baseElement, getByPlaceholderText } = renderWithProvider( + , + ); + + const input = getByPlaceholderText(PLACEHOLDER_MOCK); + + await act(async () => { + fireEvent.click(input); + }); + + expect(baseElement).toMatchSnapshot(); + }); + + it('calls onChange with primary label on option click', async () => { + const onChangeMock = jest.fn(); + + const { getByPlaceholderText, getByText } = renderWithProvider( + , + ); + + const input = getByPlaceholderText(PLACEHOLDER_MOCK); + + await act(async () => { + fireEvent.click(input); + }); + + const option = getByText(OPTIONS_MOCK[0].primaryLabel); + + await act(async () => { + fireEvent.click(option); + }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + expect(onChangeMock).toHaveBeenCalledWith(OPTIONS_MOCK[0].primaryLabel); + }); + + it('calls onChange with empty string on clear button click', async () => { + const onChangeMock = jest.fn(); + + const { getByLabelText } = renderWithProvider( + , + ); + + const clearButton = getByLabelText('[clear]'); + + await act(async () => { + fireEvent.click(clearButton); + }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + expect(onChangeMock).toHaveBeenCalledWith(''); + }); +}); diff --git a/ui/components/app/form-combo-field/form-combo-field.tsx b/ui/components/app/form-combo-field/form-combo-field.tsx new file mode 100644 index 000000000000..978a180fd83b --- /dev/null +++ b/ui/components/app/form-combo-field/form-combo-field.tsx @@ -0,0 +1,222 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import classnames from 'classnames'; +import { + ButtonIcon, + ButtonIconSize, + FormTextField, + IconName, +} from '../../component-library'; +import { I18nContext } from '../../../contexts/i18n'; +import { Display, IconColor } from '../../../helpers/constants/design-system'; + +export interface FormComboFieldOption { + primaryLabel: string; + secondaryLabel?: string; +} + +export interface FormComboFieldProps { + maxDropdownHeight?: number; + noOptionsText?: string; + onChange?: (value: string) => void; + onOptionClick?: (option: FormComboFieldOption) => void; + options: FormComboFieldOption[]; + placeholder?: string; + value: string; +} + +function Option({ + option, + onClick, +}: { + option: FormComboFieldOption; + onClick: (option: FormComboFieldOption) => void; +}) { + const handleClick = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + + onClick(option); + }, + [onClick, option], + ); + + const { primaryLabel, secondaryLabel } = option; + + return ( +
+ {primaryLabel} + {secondaryLabel ? ( + + {secondaryLabel} + + ) : null} +
+ ); +} + +function Dropdown({ + maxDropdownHeight, + noOptionsText, + onOptionClick, + options, + width, +}: { + maxDropdownHeight?: number; + noOptionsText?: string; + onOptionClick: (option?: FormComboFieldOption) => void; + options: FormComboFieldOption[]; + width: number; +}) { + const t = useContext(I18nContext); + const ref = useRef(); + const maxHeight = maxDropdownHeight ?? 179; + const [dropdownHeight, setDropdownHeight] = useState(0); + + useEffect(() => { + setDropdownHeight(ref.current?.scrollHeight ?? 0); + }); + + return ( +
maxHeight, + })} + > + {options.length === 0 && ( +
+ ); +} + +export default function FormComboField({ + maxDropdownHeight, + noOptionsText, + onChange, + onOptionClick, + options, + placeholder, + value, +}: FormComboFieldProps) { + const [dropdownVisible, setDropdownVisible] = useState(false); + const valueRef = useRef(); + const [valueWidth, setValueWidth] = useState(0); + const inputRef = useRef(null); + const t = useContext(I18nContext); + + useEffect(() => { + setValueWidth(valueRef.current?.offsetWidth); + }); + + const handleBlur = useCallback( + (e?: any) => { + if (e?.relatedTarget?.className !== 'form-combo-field__option') { + setDropdownVisible(false); + } + }, + [setDropdownVisible], + ); + + const handleChange = useCallback( + (e: any) => { + onChange?.(e.target.value); + }, + [onChange], + ); + + const handleOptionClick = useCallback( + (option) => { + setDropdownVisible(false); + + if (option) { + handleChange({ target: { value: option.primaryLabel } }); + onOptionClick?.(option); + } + + inputRef.current?.focus(); + }, + [setDropdownVisible, handleChange], + ); + + const handleClearClick = useCallback(() => { + handleChange({ target: { value: '' } }); + inputRef.current?.focus(); + }, [handleChange]); + + return ( +
+
{ + setDropdownVisible(true); + }} + > + {/* @ts-ignore */} + { + if (e.key === 'Enter') { + handleBlur(); + } + }} + value={value} + onChange={handleChange} + className={classnames({ + 'form-combo-field__value': true, + 'form-combo-field__value-dropdown-visible': dropdownVisible, + })} + endAccessory={ + handleClearClick()} + color={IconColor.iconMuted} + ariaLabel={t('clear')} + /> + } + /> +
+ {dropdownVisible && ( + + )} +
+ ); +} diff --git a/ui/components/app/form-combo-field/index.scss b/ui/components/app/form-combo-field/index.scss new file mode 100644 index 000000000000..cd56dbb1483a --- /dev/null +++ b/ui/components/app/form-combo-field/index.scss @@ -0,0 +1,58 @@ +.form-combo-field { + + width: 100%; + + ::-webkit-scrollbar-thumb { + -webkit-border-radius: 8px; + border-radius: 8px; + background: var(--color-icon-muted); + } + + ::-webkit-scrollbar { + width: 0px; + } + + &__value > div { + outline: 0; + width: 100%; + } + + &__value-dropdown-visible > div { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + &__dropdown { + border: 1px solid var(--color-border-default); + border-top-width: 0; + position: absolute; + overflow-y: scroll; + z-index: 1; + background-color: var(--color-background-default); + + &__scroll::-webkit-scrollbar { + width: 8px; + } + } + + &__option { + display: flex; + flex-direction: column; + padding: 10px 16px 10px 16px; + line-height: normal; + font-weight: normal; + } + + &__option:hover { + background-color: var(--color-background-default-hover); + } + + &__option-primary { + padding-bottom: 4px; + } + + &__option-secondary { + color: var(--color-text-alternative); + font-size: 12px; + } +} \ No newline at end of file diff --git a/ui/components/app/form-combo-field/index.ts b/ui/components/app/form-combo-field/index.ts new file mode 100644 index 000000000000..e554bd055f28 --- /dev/null +++ b/ui/components/app/form-combo-field/index.ts @@ -0,0 +1 @@ +export { default } from './form-combo-field'; diff --git a/ui/components/app/name/__snapshots__/name.test.tsx.snap b/ui/components/app/name/__snapshots__/name.test.tsx.snap new file mode 100644 index 000000000000..5769227e4286 --- /dev/null +++ b/ui/components/app/name/__snapshots__/name.test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Name renders address with proposed name 1`] = ` +
+
+
+ + + 0xc0f...4978 + + + “ + TestProposedName + ” + +
+
+
+`; + +exports[`Name renders address with proposed name according to source priority 1`] = ` +
+
+
+ + + 0xc0f...4978 + + + “ + TestProposedName + ” + +
+
+
+`; + +exports[`Name renders address with saved name 1`] = ` +
+
+
+ + + TestName + +
+
+
+`; + +exports[`Name renders address without proposed name 1`] = ` +
+
+
+ + + 0xc0f...4979 + +
+
+
+`; diff --git a/ui/components/app/name/index.scss b/ui/components/app/name/index.scss new file mode 100644 index 000000000000..70372af01fef --- /dev/null +++ b/ui/components/app/name/index.scss @@ -0,0 +1,36 @@ +.name { + border-radius: 36px; + padding: 6px 9px 6px 9px; + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12px; + + &__missing { + background-color: var(--color-warning-muted); + } + + &__saved { + background-color: var(--color-info-muted); + } + + &__missing &__icon { + color: var(--color-warning-default); + } + + &__saved &__icon { + color: var(--color-info-default); + } + + &__value, &__proposed { + color: var(--color-warning-default); + } + + &__name { + color: var(--color-info-default); + } + + &__proposed { + font-style: italic; + } +} diff --git a/ui/components/app/name/index.ts b/ui/components/app/name/index.ts new file mode 100644 index 000000000000..f3e2e4fe0a86 --- /dev/null +++ b/ui/components/app/name/index.ts @@ -0,0 +1 @@ +export { default } from './name'; diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap new file mode 100644 index 000000000000..b26affb14044 --- /dev/null +++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap @@ -0,0 +1,365 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NameDetails renders with no saved name 1`] = ` + +
+
+
+
+
+