diff --git a/app/css/interceptor.css b/app/css/interceptor.css index 77096ee5..f2df2378 100644 --- a/app/css/interceptor.css +++ b/app/css/interceptor.css @@ -1720,6 +1720,127 @@ header:has(form[role='search']) h1 { } } +.multiline-card { + --bg-color: #484848; + --button-color: #77738ccc; + --image-size: 2.25rem; + --min-text-width: 3ch; + --pad-x: 0; + --pad-y: 0; + --gap-x: 0.5rem; + --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; + 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)); + + data { + line-height: 1em; + color: var(--text-color); + text-align: left; + min-width: var(--min-text-width); + } + + button { + font: inherit; + background: var(--bg-color); + border: 0 none; + padding: 0; + cursor: pointer; + + &:hover, &:focus { background: var(--bg-color) } + } + + > [role=img] { + grid-area: top / left / bottom / data; + align-self: center; + font-size: var(--image-size); + line-height: 1; + + & svg, img { + display: inline-block; + vertical-align: -0.15em; + } + } + + > :has(data ~ button) { + display: inline-grid; + align-items: baseline; + grid-template-columns: [left] minmax(0, min-content) [right]; + grid-template-rows: [top] min-content [bottom]; + + /* title */ + &:nth-of-type(2) { + grid-area: top / data / sub / right; + data { + font-weight: 600; + } + } + + /* subtitle */ + &:nth-of-type(3) { + grid-area: sub / data / bottom / right; + data { + font-size: 0.825em; + color: var(--disabled-text-color); + } + } + + > data { grid-area: top / left / bottom / right } + + > 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; + + &:is(.multiline-card:hover *, .multiline-card:focus-within *) { opacity: 1 } + + &:hover, &:focus { + > span { + background: white; + color: black; + + span { display: inline } + svg { display: none } + } + } + + > span { + background: var(--button-color); + color: white; + font-size: 0.8em; + font-weight: 600; + padding-inline: 0.25em; + border-radius: 2px; + text-transform: uppercase; + line-height: 1.4; + + span { + display: none; + font-size: 0.8em; + } + + svg { + display: inline-block; + vertical-align: -0.125em; + } + } + } + } +} + .tooltip { background: #222222; color: #ffffff; diff --git a/app/ts/components/simulationExplaining/SimulationSummary.tsx b/app/ts/components/simulationExplaining/SimulationSummary.tsx index f78bebab..79d8ce07 100644 --- a/app/ts/components/simulationExplaining/SimulationSummary.tsx +++ b/app/ts/components/simulationExplaining/SimulationSummary.tsx @@ -352,6 +352,7 @@ function SummarizeAddress(param: SummarizeAddressParams) { : - + { beforeAndAfter === undefined ? <> : diff --git a/app/ts/components/subcomponents/MultilineCard.tsx b/app/ts/components/subcomponents/MultilineCard.tsx new file mode 100644 index 00000000..ca78b88f --- /dev/null +++ b/app/ts/components/subcomponents/MultilineCard.tsx @@ -0,0 +1,93 @@ +import { JSX } from 'preact/jsx-runtime' +import { useSignal } from '@preact/signals' +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 + label: ActionableTextProps + note: ActionableTextProps + style?: JSX.CSSProperties +} + +export const MultilineCard = ({ icon, label, note, style }: MultilineCardProps) => { + 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 CardIcon = icon.component + const defaultAction:TextAction = { label: 'Copy', icon: () => , onClick: copyTextToClipboard } + + return ( + <> +
+ + + + + +
+ + + ) +} + +export type TextAction = { + label: string + icon: () => JSX.Element + onClick?: JSX.MouseEventHandler +} + +export type ActionableTextProps = { + displayText: string + value?: string + action: TextAction | 'noaction' | undefined +} + +type TextNodeProps = { + displayText: string, + value: string +} + +const TextNode = ({ displayText, value }: TextNodeProps) => { displayText } + +const ActionableText = ({ displayText, value, action }: ActionableTextProps) => { + const DisplayText = () => + return ( + + + { action !== undefined && action !== 'noaction' ? : <> } + + ) +} + +type TextActionProps = { + icon: () => JSX.Element + textNode: () => JSX.Element + label: string + onClick?: JSX.MouseEventHandler +} + +const TextAction = ({ textNode: DisplayText, icon: ActionIcon, label, onClick }: TextActionProps) => { + return ( + + ) +} + diff --git a/app/ts/components/subcomponents/address.tsx b/app/ts/components/subcomponents/address.tsx index 66ee9430..e686c4fc 100644 --- a/app/ts/components/subcomponents/address.tsx +++ b/app/ts/components/subcomponents/address.tsx @@ -4,9 +4,10 @@ import { checksummedAddress } from '../../utils/bigint.js' import { RenameAddressCallBack } from '../../types/user-interface-types.js' import { AddressBookEntries, AddressBookEntry } from '../../types/addressBookTypes.js' import { Website } from '../../types/websiteAccessTypes.js' -import { CopyToClipboard } from './CopyToClipboard.js' import { Blockie } from './SVGBlockie.js' import { InlineCard } from './InlineCard.js' +import { EditIcon } from './icons.js' +import { ActionableTextProps, MultilineCard, TextAction } from './MultilineCard.js' export function getActiveAddressEntry(addressToFind: bigint, activeAddresses: AddressBookEntries): AddressBookEntry { for (const info of activeAddresses) { @@ -53,73 +54,38 @@ type BigAddressParams = { readonly noCopying?: boolean readonly noEditAddress?: boolean readonly renameAddressCallBack: RenameAddressCallBack + readonly style?: JSX.CSSProperties } 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 = title !== addrString ? addrString : '' - - return
-
- { !params.noCopying && addrString !== undefined ? - - - - : - - } -
+ const subTitle = addrString && title !== addrString ? addrString : '(Not in addressbook)' -
- - - { !params.noCopying && addrString !== undefined ? - - - - : - } - - - - { !params.noCopying && addrString !== undefined && subTitle !== undefined ? - - - - : - - } -
-
-} + const renameAddressAction: TextAction = { + label: 'Edit', + icon: () => , + onClick: () => params.addressBookEntry && params.renameAddressCallBack(params.addressBookEntry) + } -const AddressTitle = ({ content, useLegibleFont }: { content: string, useLegibleFont?: boolean }) => { - return

{ content }

-} + const labelConfig: ActionableTextProps = { + displayText: title, + action: !params.noEditAddress && title !== addrString ? renameAddressAction : undefined + } + + const noteConfig: ActionableTextProps = { + displayText: subTitle, + action: addrString && subTitle !== addrString ? renameAddressAction : undefined + } -const AddressSubTitle = ({ content }: { content?: string }) => { - if (!content) return <> - return

{ content }

+ return ( + params.addressBookEntry ? : <> } } + style = { params.style } + /> + ) } type ActiveAddressParams = {