import { ChangeDetectionStrategy, Component, computed, input, numberAttribute } from '@angular/core'; /** * Loading placeholder. Hidden from assistive tech (aria-hidden); mark the loading container with * aria-busy="true" and announce completion instead. The shimmer stops under prefers-reduced-motion. */ @Component({ selector: 'au-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, template: ` @for (line of lineWidths(); track $index) { } `, host: { 'aria-hidden': 'true', '[attr.data-shape]': 'shape()' }, styles: ` :host { display: grid; gap: 0.5rem; } .au-skeleton__block { display: block; border-radius: calc(var(--au-radius) * 0.5); background: linear-gradient(90deg, var(--au-surface-2) 25%, color-mix(in srgb, var(--au-surface-2) 55%, var(--au-border)) 50%, var(--au-surface-2) 75%); background-size: 200% 100%; animation: au-shimmer 1.4s ease-in-out infinite; } :host([data-shape='circle']) .au-skeleton__block { border-radius: 50%; } @keyframes au-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } } @media (prefers-reduced-motion: reduce) { .au-skeleton__block { animation: none; } } `, }) export class AuSkeleton { readonly shape = input<'text' | 'rect' | 'circle'>('text'); /** Number of text lines; the last one is shorter, like a paragraph. */ readonly lines = input(1, { transform: numberAttribute }); readonly width = input('100%'); readonly height = input(undefined); protected readonly lineWidths = computed(() => { if (this.shape() !== 'text') return [this.width()]; const n = Math.max(1, this.lines()); return Array.from({ length: n }, (_, i) => (n > 1 && i === n - 1 ? '60%' : this.width())); }); protected readonly heightValue = computed(() => this.height() ?? (this.shape() === 'text' ? '0.9rem' : this.shape() === 'circle' ? this.width() : '6rem')); }