import { hasSlotContent } from '../../../utils/slot'; import { LitElement, html, css } from 'lit'; import { property, state } from 'lit/decorators.js'; export interface EmptyStateProps { title?: string; subtitle?: string; buttonText?: string; size?: 'sm' | 'md' | 'lg'; bordered?: boolean; rounded?: boolean; }; export class AgEmptyState extends LitElement implements EmptyStateProps { @property({ type: String }) declare title: string; @property({ type: String }) declare subtitle: string; @property({ type: String, attribute: 'button-text' }) declare buttonText: string; @property({ type: String, reflect: true }) declare size: 'sm' | 'md' | 'lg'; @property({ type: Boolean }) declare bordered: boolean; @property({ type: Boolean }) declare rounded: boolean; @state() private _hasIconSlot = false; @state() private _hasActionsSlot = false; constructor() { super(); this.title = ''; this.subtitle = ''; this.buttonText = ''; this.size = 'md'; this.bordered = false; this.rounded = false; } private _handleSlotChange(e: Event) { const slot = e.target as HTMLSlotElement; const slotName = slot.name; if (slotName === 'icon') { this._hasIconSlot = hasSlotContent(slot); } else if (slotName === 'actions') { this._hasActionsSlot = hasSlotContent(slot); } } override firstUpdated() { // Initial check for slot content // We need to defer this check to avoid "change in update" warning setTimeout(() => { const iconSlot = this.shadowRoot?.querySelector('slot[name="icon"]') as HTMLSlotElement; const actionsSlot = this.shadowRoot?.querySelector('slot[name="actions"]') as HTMLSlotElement; const hadIconSlot = this._hasIconSlot; const hadActionsSlot = this._hasActionsSlot; this._hasIconSlot = hasSlotContent(iconSlot); this._hasActionsSlot = hasSlotContent(actionsSlot); // Only request update if something changed if (hadIconSlot !== this._hasIconSlot || hadActionsSlot !== this._hasActionsSlot) { this.requestUpdate(); } }, 0); } static styles = css` :host { display: block; } .empty { display: flex; flex-direction: column; align-items: center; text-align: center; padding: var(--ag-space-8); background: var(--ag-background-secondary); } .empty-bordered { background: transparent; border: var(--ag-border-width-1) solid var(--ag-border-subtle); } .empty-rounded { border-radius: var(--ag-radius-lg); } /* Icon sizing and containment */ .icon { margin-block-end: var(--ag-space-1); color: var(--ag-neutral-400); } .icon-inner { width: var(--ag-empty-icon-size, var(--ag-space-10)); height: var(--ag-empty-icon-size, var(--ag-space-10)); display: inline-flex; align-items: center; justify-content: center; } .icon-inner > ::slotted(*) { width: 100%; height: 100%; object-fit: contain; } :host([size="sm"]) { --ag-empty-icon-size: var(--ag-space-8); } :host([size="lg"]) { --ag-empty-icon-size: var(--ag-space-12); } /* Title */ .title { font-size: var(--ag-font-size-xl); margin-block-start: 0; margin-block-end: var(--ag-space-2); line-height: var(--ag-line-height-lg); } :host([size="sm"]) .title { font-size: var(--ag-font-size-lg); } :host([size="lg"]) .title { font-size: var(--ag-font-size-2x); } /* Subtitle */ .subtitle { font-size: var(--ag-font-size-base); color: var(--ag-text-muted); /* The title will provide enough gap */ margin-block-start: 0; margin-block-end: var(--ag-space-4); line-height: var(--ag-line-height-base); } :host([size="sm"]) .subtitle { font-size: var(--ag-font-size-sm); margin-block-end: var(--ag-space-3); } :host([size="lg"]) .subtitle { font-size: var(--ag-font-size-md); margin-block-end: var(--ag-space-6); } /* Actions */ .actions { display: flex; flex-wrap: wrap; gap: var(--ag-space-2); justify-content: center; } `; render() { const classes = [ 'empty', this.bordered ? 'empty-bordered' : '', this.rounded ? 'empty-rounded' : '' ].filter(Boolean).join(' '); return html`
${!this._hasIconSlot ? html` ` : ''}
${this.title ? html`

${this.title}

` : ''} ${this.subtitle ? html`

${this.subtitle}

` : ''}
${this.buttonText && !this._hasActionsSlot ? html` ` : ''}
`; } }