diff --git a/app/css/interceptor.css b/app/css/interceptor.css index ddfa4b25..26dc9f13 100644 --- a/app/css/interceptor.css +++ b/app/css/interceptor.css @@ -1732,3 +1732,139 @@ header:has(form[role='search']) h1 { * + & { margin-left: 0.25em } } } + +.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; + ---space: 0.425rem; + --edge-roundness: 3px; + --status-backdrop-color: #303030ed; + --status-success-color: #4fb64f; + + 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; + padding: 0.3125rem 0.6875rem; + border: 0 none; + border-radius: 4px; + font-size: 0.8125rem; + white-space: nowrap; + pointer-events: none; + margin: 0; +} diff --git a/app/ts/components/simulationExplaining/SimulationSummary.tsx b/app/ts/components/simulationExplaining/SimulationSummary.tsx index 4e1a92e8..26fda912 100644 --- a/app/ts/components/simulationExplaining/SimulationSummary.tsx +++ b/app/ts/components/simulationExplaining/SimulationSummary.tsx @@ -351,6 +351,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..a66f70a4 --- /dev/null +++ b/app/ts/components/subcomponents/MultilineCard.tsx @@ -0,0 +1,94 @@ +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' +import { deepMerge } from '../../utils/json.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 +} + +type TextNodeProps = { + displayText: string, + value: string +} + +const TextNode = ({ displayText, value }: TextNodeProps) => { displayText } + +const ActionableText = ({ displayText, value, action }: ActionableTextProps) => { + const DisplayText = () => + return ( + + + { action ? : <> } + + ) +} + +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..7996677d 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,39 @@ 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: undefined + // !params.noEditAddress && subTitle !== addrString ? renameAddressAction : undefined + } -const AddressSubTitle = ({ content }: { content?: string }) => { - if (!content) return <> - return

{ content }

+ return ( + params.addressBookEntry ? : <> } } + style = { params.style } + /> + ) } type ActiveAddressParams = { diff --git a/app/ts/utils/json.ts b/app/ts/utils/json.ts index 7e323c09..f52be9e2 100644 --- a/app/ts/utils/json.ts +++ b/app/ts/utils/json.ts @@ -29,3 +29,24 @@ export function isJSON(text: string){ return false } } + +/** + * Deeply merges two objects. Properties in the `source` object will overwrite + * those in the `target` object if conflicts occur. Nested objects are merged recursively. + * + * @template T - The type of the target object. + * @param {T} target - The target object that will be merged into. + * @param {Partial} source - The source object with properties to merge into the target. + * @returns {T} - A new object that is the result of the deep merge between `target` and `source`. + */ +export function deepMerge(target: T, source: Partial): T { + if (typeof target !== 'object' || target === null) return source as T + if (typeof source !== 'object' || source === null) return target + + for (const key in source) { + if (source[key] instanceof Object && key in target) { + (target as any)[key] = deepMerge((target as any)[key], source[key] as any) + } + } + return Object.assign({}, target, source) +}