diff --git a/app/css/interceptor.css b/app/css/interceptor.css index f2df2378..dcf22ff2 100644 --- a/app/css/interceptor.css +++ b/app/css/interceptor.css @@ -415,6 +415,16 @@ a.dropdown-item.is-active, button.dropdown-item.is-active { .card-header-icon { background-color: transparent; color: var(--text-color); + + .icon:has(svg[aria-label=chevron-icon]) { + width: 1rem; + height: 1rem; + transition: transform 200ms; + + .card-header:has(+ .card-content) & { + transform: rotateX(180deg); + } + } } .card-header-icon:disabled { @@ -1721,13 +1731,14 @@ header:has(form[role='search']) h1 { } .multiline-card { - --bg-color: #484848; + --bg-color: transparent; --button-color: #77738ccc; --image-size: 2.25rem; - --min-text-width: 3ch; + --min-text-width: 0; --pad-x: 0; --pad-y: 0; --gap-x: 0.5rem; + --gap-y: 0.0625rem; --edge-roundness: 3px; font: inherit; @@ -1735,12 +1746,12 @@ header:has(form[role='search']) h1 { grid-template-columns: [left] minmax(0, min-content) [data] minmax(0, max-content) [right]; grid-template-rows: [top] min-content [sub] min-content [bottom]; column-gap: var(--gap-x); - row-gap: 2px; + row-gap: var(--gap-y); padding-block: var(--pad-y); padding-inline: var(--pad-x); background-color: var(--bg-color); border-radius: var(--edge-roundness); - min-width: calc(var(--min-text-width) + var(--image-size) + (var(-pad-x) * 2) + var(--gap-x)); + min-width: calc(var(--min-text-width) + var(--image-size) + (var(--pad-x) * 2) + var(--gap-x)); data { line-height: 1em; @@ -1754,8 +1765,8 @@ header:has(form[role='search']) h1 { background: var(--bg-color); border: 0 none; padding: 0; - cursor: pointer; + &:not(:disabled) { cursor: pointer } &:hover, &:focus { background: var(--bg-color) } } @@ -1794,7 +1805,13 @@ header:has(form[role='search']) h1 { } } - > data { grid-area: top / left / bottom / right } + > data { + grid-area: top / left / bottom / right; + + &:not(:has(+:disabled)) { + .multiline-card:hover &, .multiline-card:focus-within & { visibility: hidden } + } + } > button { grid-area: top / left / bottom / right; @@ -1802,10 +1819,13 @@ header:has(form[role='search']) h1 { align-items: baseline; grid-template-columns: minmax(0, 1fr) min-content; background-color: var(--bg-color); - opacity: 0; outline: none; + position: relative; + visibility: hidden; - &:is(.multiline-card:hover *, .multiline-card:focus-within *) { opacity: 1 } + &:not(:disabled) { + .multiline-card:hover &, .multiline-card:focus-within & { visibility: visible } + } &:hover, &:focus { > span { diff --git a/app/ts/components/pages/InterceptorAccess.tsx b/app/ts/components/pages/InterceptorAccess.tsx index 5979c8db..0f7e86bf 100644 --- a/app/ts/components/pages/InterceptorAccess.tsx +++ b/app/ts/components/pages/InterceptorAccess.tsx @@ -17,6 +17,7 @@ import { Page } from '../../types/exportedSettingsTypes.js' import { ReadonlySignal, useComputed, useSignal } from '@preact/signals' import { RpcEntries } from '../../types/rpc.js' import { ModifyAddressWindowState } from '../../types/visualizer-types.js' +import { ChevronIcon } from '../subcomponents/icons.js' function Title({ icon, title} : {icon: string | undefined, title: string}) { return @@ -52,7 +53,7 @@ function AssociatedTogether({ associatedAddresses, renameAddressCallBack }: { as }

- V +
{ !showLogs diff --git a/app/ts/components/pages/PersonalSign.tsx b/app/ts/components/pages/PersonalSign.tsx index 0de71fc8..9f46cf07 100644 --- a/app/ts/components/pages/PersonalSign.tsx +++ b/app/ts/components/pages/PersonalSign.tsx @@ -19,7 +19,7 @@ import { QuarantineReasons } from '../simulationExplaining/Transactions.js' import { GnosisSafeVisualizer } from '../simulationExplaining/customExplainers/GnosisSafeVisualizer.js' import { EditEnsNamedHashCallBack } from '../subcomponents/ens.js' import { ViewSelector, ViewSelector as Viewer } from '../subcomponents/ViewSelector.js' -import { XMarkIcon } from '../subcomponents/icons.js' +import { ChevronIcon, XMarkIcon } from '../subcomponents/icons.js' import { TransactionInput } from '../subcomponents/ParsedInputData.js' import { ErrorComponent } from '../subcomponents/Error.js' import { PendingTransactionOrSignableMessage } from '../../types/accessRequest.js' @@ -358,7 +358,7 @@ function ExtraDetails({ visualizedPersonalSignRequest, renameAddressCallBack }: Extra details

- V +
{ !showSummary @@ -387,7 +387,7 @@ function RawMessage({ visualizedPersonalSignRequest }: ExtraDetailsCardParams) { Raw message

- V +
{ !showSummary diff --git a/app/ts/components/simulationExplaining/SimulationSummary.tsx b/app/ts/components/simulationExplaining/SimulationSummary.tsx index 79d8ce07..ac94aaf5 100644 --- a/app/ts/components/simulationExplaining/SimulationSummary.tsx +++ b/app/ts/components/simulationExplaining/SimulationSummary.tsx @@ -18,7 +18,7 @@ import { Website } from '../../types/websiteAccessTypes.js' import { extractTokenEvents } from '../../background/metadataUtils.js' import { EditEnsNamedHashCallBack } from '../subcomponents/ens.js' import { EnrichedEthereumInputData } from '../../types/EnrichedEthereumData.js' -import { XMarkIcon } from '../subcomponents/icons.js' +import { ChevronIcon, XMarkIcon } from '../subcomponents/icons.js' import { TransactionInput } from '../subcomponents/ParsedInputData.js' import { sendPopupMessageToBackgroundPage } from '../../background/backgroundUtils.js' import { IntegerInput } from '../subcomponents/AutosizingInput.js' @@ -428,7 +428,7 @@ export function TokenLogAnalysisCard({ simTx, renameAddressCallBack }: TokenLogA { tokenResults.length === 0 ? `No ${ tokenEventsPlural }` : `${ tokenResults.length > 1 ? `${ upperCaseFirstCharacter(convertNumberToCharacterRepresentationIfSmallEnough(tokenResults.length)) } ${ tokenEventsPlural }` : tokenEventsSingular }` }

- V +
{ !showLogs @@ -463,7 +463,7 @@ export function NonTokenLogAnalysisCard({ simTx, addressMetaData, renameAddressC { nonTokenLogs.length === 0 ? 'No non-token events' : `${ upperCaseFirstCharacter(convertNumberToCharacterRepresentationIfSmallEnough(nonTokenLogs.length)) } non-token event${ nonTokenLogs.length > 1 ? 's' : '' }` }

- V +
{ !showLogs @@ -510,7 +510,7 @@ export function TransactionsAccountChangesCard({ simTx, renameAddressCallBack, a { numberOfChanges === 0 ? 'No changes in accounts' : `${ upperCaseFirstCharacter(convertNumberToCharacterRepresentationIfSmallEnough(numberOfChanges)) } account${ numberOfChanges > 1 ? 's' : '' } changing` }

- V +
{ !showSummary @@ -698,7 +698,7 @@ export function SimulationSummary(param: SimulationSummaryParams) { { notOwnAddresses.length === 0 ? 'No changes in other accounts' : `${ upperCaseFirstCharacter(convertNumberToCharacterRepresentationIfSmallEnough(notOwnAddresses.length)) } other account${ notOwnAddresses.length > 1 ? 's' : '' } changing` }

- V +
{ !showOtherAccountChanges @@ -755,7 +755,7 @@ export function RawTransactionDetailsCard({ transaction, renameAddressCallBack, Raw transaction information

- V +
{ !showSummary diff --git a/app/ts/components/subcomponents/MultilineCard.tsx b/app/ts/components/subcomponents/MultilineCard.tsx index ca78b88f..2eff04ab 100644 --- a/app/ts/components/subcomponents/MultilineCard.tsx +++ b/app/ts/components/subcomponents/MultilineCard.tsx @@ -4,88 +4,135 @@ import { Tooltip, TooltipConfig } from './Tooltip.js' import { clipboardCopy } from './clipboardcopy.js' import { CopyIcon } from './icons.js' -export type CardIcon = { - component: () => JSX.Element - onClick?: () => void - tooltipText?: string -} - export type MultilineCardProps = { - icon: CardIcon + icon: ActionableIconProps label: ActionableTextProps note: ActionableTextProps style?: JSX.CSSProperties } export const MultilineCard = ({ icon, label, note, style }: MultilineCardProps) => { + return ( +
+ + + +
+ ) +} + +export type ActionableIconProps = { + onClick: 'clipboard-copy' + icon: () => JSX.Element + copyValue?: string + copySuccessMessage: string + hintText?: string +} | { + onClick: JSX.MouseEventHandler + icon: () => JSX.Element + hintText?: string +} | { + onClick: undefined + icon: () => JSX.Element +} + +const ActionableIcon = (props: ActionableIconProps) => { const tooltipConfig = useSignal(undefined) const copyTextToClipboard = async (event: JSX.TargetedMouseEvent) => { event.currentTarget.blur() await clipboardCopy(event.currentTarget.value) - tooltipConfig.value = { message: 'Copied!', x: event.clientX, y: event.clientY } + const copySuccessMessage = props.onClick === 'clipboard-copy' && props.copySuccessMessage ? props.copySuccessMessage : 'Copied!' + tooltipConfig.value = { message: copySuccessMessage, x: event.clientX, y: event.clientY } } - const CardIcon = icon.component - const defaultAction:TextAction = { label: 'Copy', icon: () => , onClick: copyTextToClipboard } + const CardIcon = props.icon + const handleClick = props.onClick ? props.onClick === 'clipboard-copy' ? copyTextToClipboard : props.onClick : undefined + const copyValue = props.onClick === 'clipboard-copy' ? props.copyValue : undefined + const hintText = props.onClick ? props.hintText : undefined return ( - <> -
- - - - - -
- - + + + ) } -export type TextAction = { - label: string - icon: () => JSX.Element - onClick?: JSX.MouseEventHandler +type TextNodeProps = { + displayText: string, + value: string } +const TextNode = ({ displayText, value }: TextNodeProps) => { displayText } + export type ActionableTextProps = { + onClick: 'clipboard-copy' + displayText: string + copyValue?: string + copySuccessMessage?: string +} | { + onClick: JSX.MouseEventHandler + displayText: string + buttonLabel: string + buttonIcon: () => JSX.Element +} | { + onClick?: undefined displayText: string - value?: string - action: TextAction | 'noaction' | undefined } -type TextNodeProps = { - displayText: string, - value: string -} +const ActionableText = (props: ActionableTextProps) => { + const tooltipConfig = useSignal(undefined) -const TextNode = ({ displayText, value }: TextNodeProps) => { displayText } + const copyTextToClipboard = async (event: JSX.TargetedMouseEvent) => { + event.currentTarget.blur() + await clipboardCopy(event.currentTarget.value) + tooltipConfig.value = { + message: props.onClick === 'clipboard-copy' && props.copySuccessMessage ? props.copySuccessMessage : 'Copied!', + x: event.clientX, + y: event.clientY + } + } + + const copyValue = props.onClick === 'clipboard-copy' && props.copyValue ? props.copyValue : props.displayText + const actionIcon = props.onClick ? props.onClick === 'clipboard-copy' ? () => : props.buttonIcon : () => <> + const actionHandler = props.onClick ? props.onClick === 'clipboard-copy' ? copyTextToClipboard : props.onClick : undefined + const actionButtonLabel = props.onClick ? props.onClick === 'clipboard-copy' ? 'Copy' : props.buttonLabel : '' + + const DisplayText = () => -const ActionableText = ({ displayText, value, action }: ActionableTextProps) => { - const DisplayText = () => return ( - { action !== undefined && action !== 'noaction' ? : <> } + + ) } type TextActionProps = { - icon: () => JSX.Element + onClick: undefined textNode: () => JSX.Element - label: string - onClick?: JSX.MouseEventHandler +} | { + onClick: JSX.MouseEventHandler + textNode: () => JSX.Element + buttonLabel: string + buttonIcon: () => JSX.Element + copyValue?: string } -const TextAction = ({ textNode: DisplayText, icon: ActionIcon, label, onClick }: TextActionProps) => { +const TextAction = (props: TextActionProps) => { + const DisplayText = props.textNode + const ActionIcon = props.onClick ? props.buttonIcon : () => <> + return ( - ) diff --git a/app/ts/components/subcomponents/address.tsx b/app/ts/components/subcomponents/address.tsx index e686c4fc..2d71fb00 100644 --- a/app/ts/components/subcomponents/address.tsx +++ b/app/ts/components/subcomponents/address.tsx @@ -7,7 +7,7 @@ import { Website } from '../../types/websiteAccessTypes.js' import { Blockie } from './SVGBlockie.js' import { InlineCard } from './InlineCard.js' import { EditIcon } from './icons.js' -import { ActionableTextProps, MultilineCard, TextAction } from './MultilineCard.js' +import { ActionableIconProps, ActionableTextProps, MultilineCard } from './MultilineCard.js' export function getActiveAddressEntry(addressToFind: bigint, activeAddresses: AddressBookEntries): AddressBookEntry { for (const info of activeAddresses) { @@ -40,7 +40,7 @@ export function AddressIcon(param: AddressIconParams) { if (param.logoUri !== undefined) { return ( - + ) } @@ -58,34 +58,38 @@ type BigAddressParams = { } export function BigAddress(params: BigAddressParams) { - const addrString = params.addressBookEntry && checksummedAddress(params.addressBookEntry.address) - const title = params.addressBookEntry === undefined ? 'No address found' : params.addressBookEntry.name - const subTitle = addrString && title !== addrString ? addrString : '(Not in addressbook)' - - const renameAddressAction: TextAction = { - label: 'Edit', - icon: () => , - onClick: () => params.addressBookEntry && params.renameAddressCallBack(params.addressBookEntry) + const addressString = params.addressBookEntry && checksummedAddress(params.addressBookEntry.address) + const labelText = params.addressBookEntry?.name || addressString || 'No address found' + const noteText = addressString && addressString !== labelText ? addressString : '(Not in addressbook)' + + const configPartialWithEditOnClick = { + onClick: () => params.addressBookEntry && params.renameAddressCallBack(params.addressBookEntry), + buttonLabel: 'Edit', + buttonIcon: () => + } + + const configPartialWithCopyOnClick = { + onClick: 'clipboard-copy' as const, + copyValue: addressString, + copySuccessMessage: 'Address copied!' } const labelConfig: ActionableTextProps = { - displayText: title, - action: !params.noEditAddress && title !== addrString ? renameAddressAction : undefined + displayText: labelText, + ...(labelText === addressString && !params.noCopying) ? configPartialWithCopyOnClick : configPartialWithEditOnClick } const noteConfig: ActionableTextProps = { - displayText: subTitle, - action: addrString && subTitle !== addrString ? renameAddressAction : undefined + displayText: noteText, + ...(noteText === addressString && !params.noCopying) ? configPartialWithCopyOnClick : configPartialWithEditOnClick + } + + const iconConfig: ActionableIconProps = { + icon: () => params.addressBookEntry ? : <>, + ...(!params.noCopying && addressString) ? configPartialWithCopyOnClick : { onClick: undefined } } - return ( - params.addressBookEntry ? : <> } } - style = { params.style } - /> - ) + return } type ActiveAddressParams = { @@ -130,13 +134,13 @@ export function SmallAddress({ addressBookEntry, renameAddressCallBack, style }: return <> } - return renameAddressCallBack(addressBookEntry) } style = { style } /> + return renameAddressCallBack(addressBookEntry) } style = { style } /> } -export function WebsiteOriginText( { icon, websiteOrigin, title }: Website) { +export function WebsiteOriginText({ icon, websiteOrigin, title }: Website) { return
- { icon === undefined ? <> : } + { icon === undefined ? <> : }
diff --git a/app/ts/components/subcomponents/icons.tsx b/app/ts/components/subcomponents/icons.tsx index 4799ae61..0313b1aa 100644 --- a/app/ts/components/subcomponents/icons.tsx +++ b/app/ts/components/subcomponents/icons.tsx @@ -54,3 +54,5 @@ export const CopyIcon = () => export const CheckIcon = () => + +export const ChevronIcon = () => diff --git a/app/ts/components/ui-utils.tsx b/app/ts/components/ui-utils.tsx index 49f3191e..6cb9e9fd 100644 --- a/app/ts/components/ui-utils.tsx +++ b/app/ts/components/ui-utils.tsx @@ -9,6 +9,7 @@ import { checksummedAddress } from '../utils/bigint.js' import { PopupOrTabId } from '../types/websiteAccessTypes.js' import { checkAndThrowRuntimeLastError, safeGetTab, safeGetWindow, updateTabIfExists, updateWindowIfExists } from '../utils/requests.js' import { ChainEntry, RpcEntries } from '../types/rpc.js' +import { CHAIN_NAMES } from '../utils/chainNames.js' function assertIsNode(e: EventTarget | null): asserts e is Node { if (!e || !('nodeType' in e)) { @@ -177,5 +178,11 @@ export const getAddressBookEntryOrAFiller = (addressMetaData: readonly AddressBo } export const rpcEntriesToChainEntriesWithAllChainsEntry = (rpcEntries: RpcEntries): readonly ChainEntry[] => { - return [ ...rpcEntries.map((rpcEntry) => ({ name: rpcEntry.name, chainId: rpcEntry.chainId })), { name: 'All Chains', chainId: 'AllChains' as const }] + const entries = rpcEntries.map(({ chainId }): [string, ChainEntry] => { + const chainIdString = chainId.toString() + return [chainIdString, { chainId, name: CHAIN_NAMES.get(chainIdString) || `Chain ID: ${ chainIdString }` }] + }) + const chainsMap = new Map(entries) + chainsMap.set('AllChains', { name: 'All Chains', chainId: 'AllChains' }) + return [...chainsMap.values()] }