/**
* @fileoverview UI Skeleton Component - Loading placeholder with animations
*
* @module ct-skeleton
* @description
* A loading placeholder component that displays an animated skeleton screen
* while content is being loaded. Helps improve perceived performance by
* showing users where content will appear. Supports different variants
* and custom sizing.
*
* @example
* ```html
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
import { css, html } from "lit";
import { styleMap } from "lit/directives/style-map.js";
import { BaseElement } from "../../core/base-element.ts";
export type SkeletonVariant = "default" | "text" | "circular";
/**
* CTSkeleton displays an animated loading placeholder.
*
* @tag ct-skeleton
* @extends BaseElement
*
* @property {SkeletonVariant} variant - Visual style variant ("default" | "text" | "circular")
* @property {boolean} animated - Whether to show loading animation
* @property {string|null} width - Custom width (CSS value)
* @property {string|null} height - Custom height (CSS value)
*
* @attribute {string} variant - Sets the visual style variant
* @attribute {boolean} animated - Enables/disables animation (default: true)
* @attribute {string} width - Sets custom width
* @attribute {string} height - Sets custom height
*
* @csspart skeleton - The skeleton element
*
* @note Style with CSS width/height or use width/height attributes
* @note Has role="status" and aria-label="Loading" for accessibility
*/
export class CTSkeleton extends BaseElement {
static override styles = css`
:host {
display: inline-block;
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
.skeleton {
display: block;
background-color: var(--skeleton-bg, hsl(0, 0%, 90%));
position: relative;
overflow: hidden;
}
/* Variants */
.skeleton.default {
border-radius: var(--radius, 0.375rem);
}
.skeleton.text {
border-radius: var(--radius, 0.375rem);
height: 1em;
transform: scaleY(0.8);
transform-origin: center;
}
.skeleton.circular {
border-radius: 50%;
}
/* Animation */
.skeleton.animate::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent,
var(--skeleton-shimmer, rgba(255, 255, 255, 0.5)),
transparent
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
from {
transform: translateX(-100%);
}
to {
transform: translateX(100%);
}
}
/* Screen reader only text */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
`;
static override properties = {
variant: { type: String },
animated: { type: Boolean },
width: { type: String },
height: { type: String },
};
declare variant: SkeletonVariant;
declare animated: boolean;
declare width: string | null;
declare height: string | null;
constructor() {
super();
this.variant = "default";
this.animated = true;
this.width = null;
this.height = null;
}
override render() {
const styles = {
...(this.width && { width: this.width }),
...(this.height && { height: this.height }),
};
return html`
Loading...
`;
}
}
globalThis.customElements.define("ct-skeleton", CTSkeleton);