Skip to content

Commit

Permalink
Merge branch 'main' into better-dropdown-icons
Browse files Browse the repository at this point in the history
  • Loading branch information
KillariDev authored Nov 25, 2024
2 parents 0e90ee7 + e5820bd commit 70305f7
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 74 deletions.
26 changes: 18 additions & 8 deletions app/css/interceptor.css
Original file line number Diff line number Diff line change
Expand Up @@ -1731,26 +1731,27 @@ 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;
display: inline-grid;
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;
Expand All @@ -1764,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) }
}

Expand Down Expand Up @@ -1804,18 +1805,27 @@ 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;
display: inline-grid;
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 {
Expand Down
127 changes: 87 additions & 40 deletions app/ts/components/subcomponents/MultilineCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<figure class = 'multiline-card' role = 'figure' style = { style }>
<ActionableIcon { ...icon } />
<ActionableText { ...label } />
<ActionableText { ...note } />
</figure>
)
}

export type ActionableIconProps = {
onClick: 'clipboard-copy'
icon: () => JSX.Element
copyValue?: string
copySuccessMessage: string
hintText?: string
} | {
onClick: JSX.MouseEventHandler<HTMLButtonElement>
icon: () => JSX.Element
hintText?: string
} | {
onClick: undefined
icon: () => JSX.Element
}

const ActionableIcon = (props: ActionableIconProps) => {
const tooltipConfig = useSignal<TooltipConfig | undefined>(undefined)

const copyTextToClipboard = async (event: JSX.TargetedMouseEvent<HTMLButtonElement>) => {
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: () => <CopyIcon />, 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 (
<>
<figure class = 'multiline-card' role = 'figure' style = { style }>
<span role = 'img'>
<button type = 'button' onClick = { icon.onClick || copyTextToClipboard } tabIndex = { -1 } title = { icon.tooltipText || label.displayText }><CardIcon /></button>
</span>
<ActionableText { ...label } action = { !label.action ? defaultAction : label.action } />
<ActionableText { ...note } action = { !note.action ? defaultAction : note.action } />
</figure>
<Tooltip config = { tooltipConfig } />
</>
<span role = 'img'>
<button type = 'button' onClick = { handleClick } tabIndex = { -1 } value = { copyValue } title = { hintText } disabled = { !props.onClick }>
<CardIcon />
<Tooltip config = { tooltipConfig } />
</button>
</span>
)
}

export type TextAction = {
label: string
icon: () => JSX.Element
onClick?: JSX.MouseEventHandler<HTMLButtonElement>
type TextNodeProps = {
displayText: string,
value: string
}

const TextNode = ({ displayText, value }: TextNodeProps) => <data class = 'truncate text-legible' value = { value || displayText }>{ displayText }</data>

export type ActionableTextProps = {
onClick: 'clipboard-copy'
displayText: string
copyValue?: string
copySuccessMessage?: string
} | {
onClick: JSX.MouseEventHandler<HTMLButtonElement>
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<TooltipConfig | undefined>(undefined)

const TextNode = ({ displayText, value }: TextNodeProps) => <data class = 'truncate text-legible' value = { value || displayText }>{ displayText }</data>
const copyTextToClipboard = async (event: JSX.TargetedMouseEvent<HTMLButtonElement>) => {
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' ? () => <CopyIcon /> : 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 = () => <TextNode displayText = { props.displayText } value = { copyValue } />

const ActionableText = ({ displayText, value, action }: ActionableTextProps) => {
const DisplayText = () => <TextNode displayText = { displayText } value = { value || displayText } />
return (
<span>
<DisplayText />
{ action !== undefined && action !== 'noaction' ? <TextAction { ...action } textNode = { DisplayText } /> : <></> }
<TextAction buttonLabel = { actionButtonLabel } textNode = { DisplayText } buttonIcon = { actionIcon } onClick = { actionHandler } copyValue = { copyValue } />
<Tooltip config = { tooltipConfig } />
</span>
)
}

type TextActionProps = {
icon: () => JSX.Element
onClick: undefined
textNode: () => JSX.Element
label: string
onClick?: JSX.MouseEventHandler<HTMLButtonElement>
} | {
onClick: JSX.MouseEventHandler<HTMLButtonElement>
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 (
<button type = 'button' onClick = { onClick } value = { '' } tabIndex = { 1 }>
<button type = 'button' onClick = { props.onClick } value = { props.onClick ? props.copyValue : undefined } tabIndex = { 1 } disabled = { !props.onClick }>
<DisplayText />
<span>
<ActionIcon />
<span>{ label }</span>
<span>{ props.onClick ? props.buttonLabel : '' }</span>
</span>
</button>
)
Expand Down
54 changes: 29 additions & 25 deletions app/ts/components/subcomponents/address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -40,7 +40,7 @@ export function AddressIcon(param: AddressIconParams) {
if (param.logoUri !== undefined) {
return (
<AddressIconFrame isBig = { param.isBig }>
<img src = { param.logoUri } style = { { display: 'block', width: '1em', minWidth: '1em', height: '1em' } }/>
<img src = { param.logoUri } style = { { display: 'block', width: '1em', minWidth: '1em', height: '1em' } } />
</AddressIconFrame>
)
}
Expand All @@ -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: () => <EditIcon />,
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: () => <EditIcon />
}

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 ? <Blockie address = { params.addressBookEntry.address } /> : <></>,
...(!params.noCopying && addressString) ? configPartialWithCopyOnClick : { onClick: undefined }
}

return (
<MultilineCard
label = { labelConfig }
note = { noteConfig }
icon = { { component: () => params.addressBookEntry ? <Blockie address = { params.addressBookEntry.address } /> : <></> } }
style = { params.style }
/>
)
return <MultilineCard label = { labelConfig } note = { noteConfig } icon = { iconConfig } style = { params.style } />
}

type ActiveAddressParams = {
Expand Down Expand Up @@ -130,13 +134,13 @@ export function SmallAddress({ addressBookEntry, renameAddressCallBack, style }:
return <></>
}

return <InlineCard label={ addressBookEntry.name } copyValue = { addressString } icon={ generateIcon } onEditClicked={ () => renameAddressCallBack(addressBookEntry) } style = { style } />
return <InlineCard label = { addressBookEntry.name } copyValue = { addressString } icon = { generateIcon } onEditClicked = { () => renameAddressCallBack(addressBookEntry) } style = { style } />
}

export function WebsiteOriginText( { icon, websiteOrigin, title }: Website) {
export function WebsiteOriginText({ icon, websiteOrigin, title }: Website) {
return <div class = 'card-header-icon unsetcursor' style = 'width: 100%; padding: 0'>
<span style = 'width: 24px; height: 24px; min-width: 24px'>
{ icon === undefined ? <></> : <img src = { icon } style = 'width: 24px; height: 24px;'/> }
{ icon === undefined ? <></> : <img src = { icon } style = 'width: 24px; height: 24px;' /> }
</span>

<div class = 'media-content' style = 'overflow-y: hidden; overflow-x: clip; display: block; padding-left: 10px;'>
Expand Down
9 changes: 8 additions & 1 deletion app/ts/components/ui-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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<string, ChainEntry>(entries)
chainsMap.set('AllChains', { name: 'All Chains', chainId: 'AllChains' })
return [...chainsMap.values()]
}

0 comments on commit 70305f7

Please sign in to comment.