diff --git a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html index 6aedae5..b58120c 100644 --- a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html +++ b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.html @@ -113,7 +113,14 @@

Price for {{ price.product }}

Trial Period Days
- Legacy + +
+ Legacy + Info icon +
+
@@ -161,7 +168,13 @@

Pricing

Type - Flat rate + + Flat rate + + + Currency @@ -202,12 +215,24 @@

Currencies

Upsells

-
Boosts revenue
+ +
+ Boosts revenue + Info icon +
+
Upsells to + +
diff --git a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts index c6f01e9..03d8e4b 100644 --- a/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts +++ b/apps/web/src/app/features/account/products/views/price-detail/price-detail.component.ts @@ -13,6 +13,7 @@ import { PriceActionsHostComponent } from '../../components/price-actions-host/p import { ActivatedRoute, Router } from '@angular/router'; import { PopupMenuAction, PopupMenuComponent } from '../../../../../shared'; import { EventsListComponent } from '../../../components'; +import { MoreInfoHoverComponent } from '../../../../../shared'; import { Subscription } from 'rxjs'; @@ -24,6 +25,7 @@ import { Subscription } from 'rxjs'; DecimalPipe, UpperCasePipe, EventsListComponent, + MoreInfoHoverComponent, ], templateUrl: './price-detail.component.html', styleUrl: './price-detail.component.scss', diff --git a/apps/web/src/app/shared/ui/index.ts b/apps/web/src/app/shared/ui/index.ts index e5264c6..3b09b90 100644 --- a/apps/web/src/app/shared/ui/index.ts +++ b/apps/web/src/app/shared/ui/index.ts @@ -9,3 +9,4 @@ export * from './status-chip/status-chip.component'; export * from './test-mode-banner/test-mode-banner.component'; export * from './confirm-dialog/confirm-dialog.component'; export * from './popup-menu/popup-menu.component'; +export * from './more-info-hover/more-info-hover.component'; diff --git a/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.html b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.html new file mode 100644 index 0000000..18e21ae --- /dev/null +++ b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.html @@ -0,0 +1,24 @@ + + + + + + + + @if (text) { + {{ text }} + } @if (linkUrl) { + {{ linkText }} + } + + + diff --git a/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.scss b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.scss new file mode 100644 index 0000000..173d7dd --- /dev/null +++ b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.scss @@ -0,0 +1,130 @@ +@use '../../../styles/base.scss' as *; + +$bubble-gap: $spacing-small; +$bubble-tail-size: 7px; +$bubble-tail-width: 10px; + +:host { + display: inline-flex; + align-items: center; + line-height: 0; +} + +.more-info-wrapper { + position: relative; + display: inline-flex; + align-items: center; + line-height: 0; + outline: none; +} + +.more-info-default-icon { + transition: opacity $transition; +} + +.more-info-wrapper:hover .more-info-default-icon, +.more-info-wrapper:focus-visible .more-info-default-icon { + opacity: 1; +} + +.more-info-bubble { + position: absolute; + left: 50%; + z-index: 30; + min-width: 280px; + max-width: 360px; + padding: $spacing-snug $spacing; + background-color: $background-color; + border: 1px solid $darkest-background-color; + border-radius: $border-radius-small; + font-size: $font-size; + line-height: 1.45; + color: $text-color; + text-align: left; + white-space: normal; + opacity: 0; + visibility: hidden; + transition: opacity $transition-fast ease-out, + transform $transition-fast ease-out, visibility 0s linear $transition-fast; + + a { + display: inline-block; + margin-top: $spacing-extra-small; + font-size: $font-size; + } + + span + a { + margin-left: $spacing-extra-small; + } + + // Transparent bridge across the visual gap so the cursor stays inside the + // wrapper while moving from the trigger to the bubble (and back). + &::before { + content: ''; + position: absolute; + left: 0; + right: 0; + height: $bubble-gap; + } + + // The tail/pointer. A CSS triangle drawn with borders; drop-shadow paints + // the matching outline so it lines up with the bubble border. + &::after { + content: ''; + position: absolute; + left: 50%; + width: 0; + height: 0; + margin-left: -#{$bubble-tail-width}; + border-left: $bubble-tail-width solid transparent; + border-right: $bubble-tail-width solid transparent; + } +} + +.more-info-bubble-bottom { + top: 100%; + transform: translateX(-50%) translateY(calc(#{$bubble-gap} + 4px)); + + &::before { + top: -$bubble-gap; + } + + &::after { + top: -$bubble-tail-size; + border-bottom: $bubble-tail-size solid $background-color; + filter: drop-shadow(0 -1px 0 $darkest-background-color); + } +} + +.more-info-bubble-top { + bottom: 100%; + transform: translateX(-50%) translateY(calc(-1 * (#{$bubble-gap} + 4px))); + + &::before { + bottom: -$bubble-gap; + } + + &::after { + bottom: -$bubble-tail-size; + border-top: $bubble-tail-size solid $background-color; + filter: drop-shadow(0 1px 0 $darkest-background-color); + } +} + +.more-info-wrapper:hover .more-info-bubble, +.more-info-wrapper:focus-within .more-info-bubble { + opacity: 1; + visibility: visible; + transition: opacity $transition-fast ease-out, + transform $transition-fast ease-out, visibility 0s linear 0s; +} + +.more-info-wrapper:hover .more-info-bubble-bottom, +.more-info-wrapper:focus-within .more-info-bubble-bottom { + transform: translateX(-50%) translateY($bubble-gap); +} + +.more-info-wrapper:hover .more-info-bubble-top, +.more-info-wrapper:focus-within .more-info-bubble-top { + transform: translateX(-50%) translateY(calc(-1 * #{$bubble-gap})); +} diff --git a/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.ts b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.ts new file mode 100644 index 0000000..eae483d --- /dev/null +++ b/apps/web/src/app/shared/ui/more-info-hover/more-info-hover.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-more-info-hover', + imports: [], + templateUrl: './more-info-hover.component.html', + styleUrl: './more-info-hover.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MoreInfoHoverComponent { + /** Body text shown inside the hover bubble. Ignored if a [bubble] slot is projected. */ + @Input() text = ''; + /** Optional link label shown after the body text. Defaults to "Learn more" when a url is given. */ + @Input() linkText = 'Learn more'; + /** Optional url for the link. */ + @Input() linkUrl = ''; + /** Accessible label applied to the default info icon trigger. */ + @Input() ariaLabel = 'More info'; + /** Where the bubble appears relative to the trigger. */ + @Input() position: 'top' | 'bottom' = 'top'; +}