mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 2058: feat(crm): customer card copy-to-clipboard and carousel improvements
Customer Card Copy-to-Clipboard (#5508) - Click on card number copies it to clipboard using Angular CDK Clipboard - Shows success tooltip confirmation positioned on the right - Tooltip auto-dismisses after 3 seconds Card Stack Carousel Improvements (#5509) - Fix card centering by using afterNextRender instead of AfterViewInit - Add ResizeObserver to handle dynamic size changes - Disable transforms until natural position is measured (prevents initial jump) - Center single card in carousel view Tooltip Enhancements - Add success variant with green styling (isa-accent-green) - Add position input (left | right | top | bottom) - Add fade in/out CSS keyframes animations (150ms) - Respect prefers-reduced-motion for accessibility Related Tasks - Closes #5508 - Refs #5509
This commit is contained in:
committed by
Nino Righi
parent
7950994d66
commit
a5bb8b2895
@@ -15,8 +15,19 @@
|
|||||||
Kundenkarte Nr.:
|
Kundenkarte Nr.:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card number (white, large) -->
|
<!-- Card number (white, large) - click to copy -->
|
||||||
<div class="isa-text-subtitle-1-bold text-center text-isa-white">
|
<div
|
||||||
|
class="isa-text-subtitle-1-bold text-center text-isa-white cursor-pointer"
|
||||||
|
(click)="copyCode($event)"
|
||||||
|
data-what="card-code"
|
||||||
|
[attr.data-which]="card().code"
|
||||||
|
uiTooltip
|
||||||
|
#copyTooltip="uiTooltip"
|
||||||
|
content="Kundenkarte Nr. kopiert!"
|
||||||
|
variant="success"
|
||||||
|
position="right"
|
||||||
|
[triggerOn]="[]"
|
||||||
|
>
|
||||||
{{ card().code }}
|
{{ card().code }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
inject,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
viewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { BonusCardInfo } from '@isa/crm/data-access';
|
import { BonusCardInfo } from '@isa/crm/data-access';
|
||||||
import { BarcodeComponent } from '@isa/shared/barcode';
|
import { BarcodeComponent } from '@isa/shared/barcode';
|
||||||
|
import { TooltipDirective } from '@isa/ui/tooltip';
|
||||||
import { LockCustomerCardComponent } from '../lock-customer-card/lock-customer-card.component';
|
import { LockCustomerCardComponent } from '../lock-customer-card/lock-customer-card.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,12 +33,16 @@ import { LockCustomerCardComponent } from '../lock-customer-card/lock-customer-c
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'crm-customer-card',
|
selector: 'crm-customer-card',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './customer-card.component.html',
|
templateUrl: './customer-card.component.html',
|
||||||
styleUrl: './customer-card.component.css',
|
styleUrl: './customer-card.component.css',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [BarcodeComponent, LockCustomerCardComponent],
|
imports: [BarcodeComponent, LockCustomerCardComponent, TooltipDirective],
|
||||||
})
|
})
|
||||||
export class CustomerCardComponent {
|
export class CustomerCardComponent {
|
||||||
|
readonly #clipboard = inject(Clipboard);
|
||||||
|
private readonly copyTooltip = viewChild<TooltipDirective>('copyTooltip');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bonus card data to display.
|
* Bonus card data to display.
|
||||||
*/
|
*/
|
||||||
@@ -53,4 +61,18 @@ export class CustomerCardComponent {
|
|||||||
protected readonly barcodeHeight = 4.5 * 16; // 4.5rem = 72px
|
protected readonly barcodeHeight = 4.5 * 16; // 4.5rem = 72px
|
||||||
protected readonly barcodeWidth = 0.125 * 16; // 0.125rem = 2px
|
protected readonly barcodeWidth = 0.125 * 16; // 0.125rem = 2px
|
||||||
protected readonly barcodeMargin = 0.5 * 16; // 0.5rem = 8px
|
protected readonly barcodeMargin = 0.5 * 16; // 0.5rem = 8px
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the card code to the clipboard and shows a tooltip confirmation.
|
||||||
|
*/
|
||||||
|
copyCode(event: Event): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.#clipboard.copy(this.card().code);
|
||||||
|
|
||||||
|
const tooltip = this.copyTooltip();
|
||||||
|
if (tooltip) {
|
||||||
|
tooltip.show();
|
||||||
|
setTimeout(() => tooltip.hide(), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
AfterViewInit,
|
afterNextRender,
|
||||||
computed,
|
computed,
|
||||||
Directive,
|
Directive,
|
||||||
effect,
|
|
||||||
ElementRef,
|
ElementRef,
|
||||||
inject,
|
inject,
|
||||||
|
OnDestroy,
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CardStackContainerDirective } from './card-stack-container.directive';
|
import { CardStackContainerDirective } from './card-stack-container.directive';
|
||||||
@@ -13,12 +13,17 @@ import { CardStackContainerDirective } from './card-stack-container.directive';
|
|||||||
selector: '[crmCardStackDistance]',
|
selector: '[crmCardStackDistance]',
|
||||||
host: {
|
host: {
|
||||||
'[style.--distance-to-center.px]': 'distanceToCenter()',
|
'[style.--distance-to-center.px]': 'distanceToCenter()',
|
||||||
|
// Disable ALL transforms until we've measured the natural position
|
||||||
|
'[style.transform]': 'ready() ? null : "none"',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class CardStackDistanceDirective implements AfterViewInit {
|
export class CardStackDistanceDirective implements OnDestroy {
|
||||||
readonly container = inject(CardStackContainerDirective, { host: true });
|
readonly container = inject(CardStackContainerDirective, { host: true });
|
||||||
readonly elementRef = inject(ElementRef<HTMLElement>);
|
readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||||
|
|
||||||
|
/** True once we've measured the card's natural position */
|
||||||
|
readonly ready = signal(false);
|
||||||
|
|
||||||
distanceToCenter = computed(() => {
|
distanceToCenter = computed(() => {
|
||||||
const containerCenterX = this.container.centerX();
|
const containerCenterX = this.container.centerX();
|
||||||
const centerX = this.centerX();
|
const centerX = this.centerX();
|
||||||
@@ -26,26 +31,26 @@ export class CardStackDistanceDirective implements AfterViewInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
centerX = signal(0);
|
centerX = signal(0);
|
||||||
|
private resizeObserver?: ResizeObserver;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Recalculate position when container center changes
|
// Wait for Angular to complete rendering and layout before calculating position
|
||||||
effect(() => {
|
afterNextRender(() => {
|
||||||
// React to container center changes
|
// Measure natural position (with --distance-to-center: 0, card is in natural spot)
|
||||||
const containerCenter = this.container.centerX();
|
this.centerX.set(this.getHorizontalCenter());
|
||||||
|
// Now enable the transform
|
||||||
|
this.ready.set(true);
|
||||||
|
|
||||||
// Recalculate this card's center position
|
// Watch for this card's size changes
|
||||||
if (containerCenter !== 0) {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
// Use requestAnimationFrame to ensure DOM is stable
|
this.centerX.set(this.getHorizontalCenter());
|
||||||
requestAnimationFrame(() => {
|
});
|
||||||
this.centerX.set(this.getHorizontalCenter());
|
this.resizeObserver.observe(this.elementRef.nativeElement);
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngOnDestroy(): void {
|
||||||
// Initial calculation
|
this.resizeObserver?.disconnect();
|
||||||
this.centerX.set(this.getHorizontalCenter());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHorizontalCenter(): number {
|
getHorizontalCenter(): number {
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ crm-customer-card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Stacked state transformations */
|
/* Stacked state transformations */
|
||||||
.stacked crm-customer-card {
|
.stacked crm-customer-card,
|
||||||
|
.single-card crm-customer-card {
|
||||||
transform: translateX(var(--distance-to-center));
|
transform: translateX(var(--distance-to-center));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
<!-- Carousel with navigation arrows -->
|
<!-- Carousel with navigation arrows -->
|
||||||
<ui-carousel
|
<ui-carousel
|
||||||
[class.stacked]="stacked()"
|
[class.stacked]="stacked()"
|
||||||
|
[class.single-card]="singleCard()"
|
||||||
[gap]="stacked() ? '0rem' : '1rem'"
|
[gap]="stacked() ? '0rem' : '1rem'"
|
||||||
[arrowAutoHide]="true"
|
[arrowAutoHide]="true"
|
||||||
crmCardStackContainer
|
crmCardStackContainer
|
||||||
[disabled]="stacked()"
|
[disabled]="stacked() || singleCard()"
|
||||||
>
|
>
|
||||||
@for (card of sortedCards(); track card.code; let idx = $index) {
|
@for (card of sortedCards(); track card.code; let idx = $index) {
|
||||||
<crm-customer-card
|
<crm-customer-card
|
||||||
|
|||||||
@@ -59,6 +59,33 @@ describe('CustomerCardsCarouselComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('singleCard computed signal', () => {
|
||||||
|
it('should return true when there is exactly one card', () => {
|
||||||
|
const singleCard: BonusCardInfo[] = [
|
||||||
|
{ code: 'CARD-1', isActive: true, isPrimary: true, firstName: 'John', lastName: 'Doe', totalPoints: 100 } as BonusCardInfo,
|
||||||
|
];
|
||||||
|
|
||||||
|
fixture.componentRef.setInput('cards', singleCard);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.singleCard()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when there are multiple cards', () => {
|
||||||
|
fixture.componentRef.setInput('cards', mockCards);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.singleCard()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when there are no cards', () => {
|
||||||
|
fixture.componentRef.setInput('cards', []);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.singleCard()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('sortedCards computed signal', () => {
|
describe('sortedCards computed signal', () => {
|
||||||
it('should sort cards with active cards first', () => {
|
it('should sort cards with active cards first', () => {
|
||||||
fixture.componentRef.setInput('cards', mockCards);
|
fixture.componentRef.setInput('cards', mockCards);
|
||||||
|
|||||||
@@ -38,13 +38,25 @@ import { CardStackDistanceDirective } from './card-stack-distance.directive';
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class CustomerCardsCarouselComponent {
|
export class CustomerCardsCarouselComponent {
|
||||||
stacked = signal(true);
|
#stacked = signal(true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All bonus cards to display in carousel.
|
* All bonus cards to display in carousel.
|
||||||
*/
|
*/
|
||||||
readonly cards = input.required<BonusCardInfo[]>();
|
readonly cards = input.required<BonusCardInfo[]>();
|
||||||
|
|
||||||
|
stacked = computed(() => {
|
||||||
|
const cards = this.cards();
|
||||||
|
const stacked = this.#stacked();
|
||||||
|
|
||||||
|
return stacked && cards.length > 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when there is exactly one card - disables carousel scrolling.
|
||||||
|
*/
|
||||||
|
readonly singleCard = computed(() => this.cards().length === 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event emitted when a card has been successfully locked or unlocked.
|
* Event emitted when a card has been successfully locked or unlocked.
|
||||||
* Parent should reload cards and transactions.
|
* Parent should reload cards and transactions.
|
||||||
@@ -74,6 +86,6 @@ export class CustomerCardsCarouselComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
unStack(): void {
|
unStack(): void {
|
||||||
this.stacked.set(false);
|
this.#stacked.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,15 +36,26 @@ export class TooltipComponent {
|
|||||||
/**
|
/**
|
||||||
* The visual variant of the tooltip.
|
* The visual variant of the tooltip.
|
||||||
*/
|
*/
|
||||||
variant = signal<'default' | 'warning'>('default');
|
variant = signal<'default' | 'warning' | 'success'>('default');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed classes for the tooltip based on variant.
|
* Whether the tooltip is in the leaving state (playing exit animation).
|
||||||
|
*/
|
||||||
|
leaving = signal(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed classes for the tooltip based on variant and leaving state.
|
||||||
*/
|
*/
|
||||||
tooltipClasses = computed(() => {
|
tooltipClasses = computed(() => {
|
||||||
const classes = ['ui-tooltip'];
|
const classes = ['ui-tooltip'];
|
||||||
if (this.variant() === 'warning') {
|
const v = this.variant();
|
||||||
|
if (v === 'warning') {
|
||||||
classes.push('ui-tooltip--warning');
|
classes.push('ui-tooltip--warning');
|
||||||
|
} else if (v === 'success') {
|
||||||
|
classes.push('ui-tooltip--success');
|
||||||
|
}
|
||||||
|
if (this.leaving()) {
|
||||||
|
classes.push('ui-tooltip--leaving');
|
||||||
}
|
}
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,9 +88,12 @@ export class TooltipDirective implements OnDestroy {
|
|||||||
#tooltipInstance: TooltipComponent | null = null;
|
#tooltipInstance: TooltipComponent | null = null;
|
||||||
#openTrigger: TooltipTrigger | null = null; // Tracks which trigger opened the tooltip
|
#openTrigger: TooltipTrigger | null = null; // Tracks which trigger opened the tooltip
|
||||||
#isOpen = signal(false); // Internal signal to track open state for CloseOnScrollDirective
|
#isOpen = signal(false); // Internal signal to track open state for CloseOnScrollDirective
|
||||||
|
#isHiding = false; // Tracks if tooltip is currently animating out
|
||||||
|
|
||||||
// Distance between tooltip and anchor element
|
// Distance between tooltip and anchor element
|
||||||
readonly #offset = 8; // 0.5rem = 8px
|
readonly #offset = 8; // 0.5rem = 8px
|
||||||
|
// Animation duration in ms (must match CSS)
|
||||||
|
readonly #animationDuration = 150;
|
||||||
|
|
||||||
/** Optional title for the tooltip. */
|
/** Optional title for the tooltip. */
|
||||||
title = input<string>();
|
title = input<string>();
|
||||||
@@ -111,7 +114,13 @@ export class TooltipDirective implements OnDestroy {
|
|||||||
* Visual variant of the tooltip.
|
* Visual variant of the tooltip.
|
||||||
* Defaults to 'default'.
|
* Defaults to 'default'.
|
||||||
*/
|
*/
|
||||||
variant = input<'default' | 'warning'>('default');
|
variant = input<'default' | 'warning' | 'success'>('default');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferred position of the tooltip relative to the host element.
|
||||||
|
* Defaults to 'left'. Falls back to other positions if preferred doesn't fit.
|
||||||
|
*/
|
||||||
|
position = input<'left' | 'right' | 'top' | 'bottom'>('left');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Set up effects to update tooltip instance when inputs change
|
// Set up effects to update tooltip instance when inputs change
|
||||||
@@ -155,45 +164,54 @@ export class TooltipDirective implements OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the connected positions for the tooltip overlay.
|
* Calculates the connected positions for the tooltip overlay.
|
||||||
* The preferred position is to the left-center of the host element.
|
* Orders positions based on the preferred position input.
|
||||||
* Fallback positions are right-center, bottom, and top.
|
|
||||||
* @returns An array of `ConnectedPosition` objects.
|
* @returns An array of `ConnectedPosition` objects.
|
||||||
*/
|
*/
|
||||||
#getPositions(): ConnectedPosition[] {
|
#getPositions(): ConnectedPosition[] {
|
||||||
return [
|
const left: ConnectedPosition = {
|
||||||
{
|
originX: 'start',
|
||||||
// Left-center position (default/preferred)
|
originY: 'center',
|
||||||
originX: 'start',
|
overlayX: 'end',
|
||||||
originY: 'center',
|
overlayY: 'center',
|
||||||
overlayX: 'end',
|
offsetX: -this.#offset,
|
||||||
overlayY: 'center',
|
};
|
||||||
offsetX: -this.#offset,
|
|
||||||
},
|
const right: ConnectedPosition = {
|
||||||
{
|
originX: 'end',
|
||||||
// Right-center position
|
originY: 'center',
|
||||||
originX: 'end',
|
overlayX: 'start',
|
||||||
originY: 'center',
|
overlayY: 'center',
|
||||||
overlayX: 'start',
|
offsetX: this.#offset,
|
||||||
overlayY: 'center',
|
};
|
||||||
offsetX: this.#offset,
|
|
||||||
},
|
const bottom: ConnectedPosition = {
|
||||||
{
|
originX: 'center',
|
||||||
// Bottom position
|
originY: 'bottom',
|
||||||
originX: 'start',
|
overlayX: 'center',
|
||||||
originY: 'bottom',
|
overlayY: 'top',
|
||||||
overlayX: 'start',
|
offsetY: this.#offset,
|
||||||
overlayY: 'top',
|
};
|
||||||
offsetY: this.#offset,
|
|
||||||
},
|
const top: ConnectedPosition = {
|
||||||
{
|
originX: 'center',
|
||||||
// Top position
|
originY: 'top',
|
||||||
originX: 'start',
|
overlayX: 'center',
|
||||||
originY: 'top',
|
overlayY: 'bottom',
|
||||||
overlayX: 'start',
|
offsetY: -this.#offset,
|
||||||
overlayY: 'bottom',
|
};
|
||||||
offsetY: -this.#offset,
|
|
||||||
},
|
// Order positions based on preferred position
|
||||||
];
|
switch (this.position()) {
|
||||||
|
case 'right':
|
||||||
|
return [right, left, bottom, top];
|
||||||
|
case 'top':
|
||||||
|
return [top, bottom, left, right];
|
||||||
|
case 'bottom':
|
||||||
|
return [bottom, top, left, right];
|
||||||
|
case 'left':
|
||||||
|
default:
|
||||||
|
return [left, right, bottom, top];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Shows the tooltip.
|
* Shows the tooltip.
|
||||||
@@ -259,9 +277,10 @@ export class TooltipDirective implements OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides the tooltip only if the trigger matches the open trigger.
|
* Hides the tooltip only if the trigger matches the open trigger.
|
||||||
|
* Plays fade-out animation before detaching.
|
||||||
*/
|
*/
|
||||||
hide(trigger?: TooltipTrigger) {
|
hide(trigger?: TooltipTrigger) {
|
||||||
if (!this.#overlayRef) {
|
if (!this.#overlayRef || this.#isHiding) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,14 +303,26 @@ export class TooltipDirective implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#overlayRef.detach();
|
// Start exit animation
|
||||||
this.#overlayRef.dispose();
|
this.#isHiding = true;
|
||||||
this.#overlayRef = null;
|
if (this.#tooltipInstance) {
|
||||||
this.#tooltipInstance = null;
|
this.#tooltipInstance.leaving.set(true);
|
||||||
this.#openTrigger = null;
|
}
|
||||||
|
|
||||||
// Update open state for CloseOnScrollDirective
|
// Wait for animation to complete before detaching
|
||||||
this.#isOpen.set(false);
|
setTimeout(() => {
|
||||||
|
if (this.#overlayRef) {
|
||||||
|
this.#overlayRef.detach();
|
||||||
|
this.#overlayRef.dispose();
|
||||||
|
this.#overlayRef = null;
|
||||||
|
}
|
||||||
|
this.#tooltipInstance = null;
|
||||||
|
this.#openTrigger = null;
|
||||||
|
this.#isHiding = false;
|
||||||
|
|
||||||
|
// Update open state for CloseOnScrollDirective
|
||||||
|
this.#isOpen.set(false);
|
||||||
|
}, this.#animationDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,17 @@
|
|||||||
shadow-[0px_2px_8px_0px_rgba(223,0,27,0.15)];
|
shadow-[0px_2px_8px_0px_rgba(223,0,27,0.15)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-tooltip--success {
|
||||||
|
@apply w-auto
|
||||||
|
max-w-[12rem]
|
||||||
|
p-3
|
||||||
|
rounded-lg
|
||||||
|
bg-green-50
|
||||||
|
border
|
||||||
|
border-isa-accent-green
|
||||||
|
shadow-[0px_2px_8px_0px_rgba(0,128,0,0.15)];
|
||||||
|
}
|
||||||
|
|
||||||
.ui-tooltip-title {
|
.ui-tooltip-title {
|
||||||
@apply isa-text-body-2-bold text-isa-neutral-900;
|
@apply isa-text-body-2-bold text-isa-neutral-900;
|
||||||
}
|
}
|
||||||
@@ -39,6 +50,14 @@
|
|||||||
@apply text-isa-accent-red text-sm;
|
@apply text-isa-accent-red text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-tooltip--success .ui-tooltip-title {
|
||||||
|
@apply text-isa-accent-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tooltip--success .ui-tooltip-content {
|
||||||
|
@apply text-isa-accent-green text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Global styles for tooltip overlay container
|
Global styles for tooltip overlay container
|
||||||
These styles will be injected into the global stylesheet
|
These styles will be injected into the global stylesheet
|
||||||
@@ -48,10 +67,46 @@
|
|||||||
pointer-events: none; /* Allow clicks to pass through */
|
pointer-events: none; /* Allow clicks to pass through */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keyframes for tooltip animations */
|
||||||
|
@keyframes tooltipFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltipFadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Animation for tooltip appearance */
|
/* Animation for tooltip appearance */
|
||||||
.ui-tooltip {
|
.ui-tooltip {
|
||||||
opacity: 1;
|
animation: tooltipFadeIn 0.15s ease-out;
|
||||||
transition: opacity 150ms ease-in-out;
|
}
|
||||||
|
|
||||||
|
.ui-tooltip--leaving {
|
||||||
|
animation: tooltipFadeOut 0.15s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Respect reduced motion preferences */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ui-tooltip {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.ui-tooltip--leaving {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-tooltip-icon {
|
.ui-tooltip-icon {
|
||||||
|
|||||||
Reference in New Issue
Block a user