// v2/lib/src/components/Timeline/core/_Timeline.ts import { LitElement, html, css, nothing } from 'lit'; import { property } from 'lit/decorators.js'; /** * Props interface for AgTimeline component */ export interface AgTimelineProps { /** Orientation of the timeline */ orientation?: 'horizontal' | 'vertical'; /** Visual variant for styling connectors and markers */ variant?: 'primary' | 'success' | 'warning' | 'danger' | 'monochrome' | ''; /** Whether to use compact spacing */ compact?: boolean; /** ARIA label for the timeline */ ariaLabel?: string | null; } /** Alias required for SDUI codegen discovery (glob: Timeline/core/_*.ts → looks for TimelineProps) */ export interface TimelineProps extends AgTimelineProps {} /** * AgTimeline - A semantic timeline component for displaying chronological events * * @element ag-timeline * * @slot - Default slot for ag-timeline-item elements * * @csspart ag-timeline-container - The main timeline container (ul element) * * @cssprop --ag-timeline-connector-color - Color of connector lines (default: var(--ag-border)) * @cssprop --ag-timeline-connector-width - Width of connector lines (default: 2px) * @cssprop --ag-timeline-vertical-spacing - Spacing between timeline items in vertical orientation (default: var(--ag-space-4)) */ export class AgTimeline extends LitElement implements AgTimelineProps { // ────────────────────────────────────────────────────────────── // Public Properties // ────────────────────────────────────────────────────────────── @property({ type: String, reflect: true }) declare orientation: 'horizontal' | 'vertical'; @property({ type: String, reflect: true }) declare variant: 'primary' | 'success' | 'warning' | 'danger' | 'monochrome' | ''; @property({ type: Boolean }) declare compact: boolean; @property({ type: String, attribute: 'aria-label' }) declare ariaLabel: string | null; // ────────────────────────────────────────────────────────────── // Constructor // ────────────────────────────────────────────────────────────── constructor() { super(); this.orientation = 'horizontal'; this.variant = ''; this.compact = false; } // ────────────────────────────────────────────────────────────── // Styles // ────────────────────────────────────────────────────────────── static styles = css` :host { display: block; } .timeline-container { list-style: none; margin: 0; padding: 0; display: flex; position: relative; } slot { display: contents; } /* Horizontal orientation (default) */ :host([orientation="horizontal"]) .timeline-container { flex-direction: row; align-items: stretch; } /* Vertical orientation */ :host([orientation="vertical"]) .timeline-container { flex-direction: column; align-items: stretch; } `; // ────────────────────────────────────────────────────────────── // Lifecycle // ────────────────────────────────────────────────────────────── override updated(changedProperties: Map) { super.updated(changedProperties); // Update child items when properties change if (changedProperties.has('orientation') || changedProperties.has('variant')) { this._updateChildItems(); } } private _updateChildItems() { const slot = this.shadowRoot?.querySelector('slot'); if (!slot) return; const items = slot.assignedElements({ flatten: true }) .filter(el => el.tagName.toLowerCase() === 'ag-timeline-item') as AgTimelineItem[]; items.forEach((item, index) => { // Pass orientation down to children item.setAttribute('orientation', this.orientation); if (this.variant) { item.setAttribute('variant', this.variant); } // Mark first and last items for styling item.toggleAttribute('first', index === 0); item.toggleAttribute('last', index === items.length - 1); }); } // ────────────────────────────────────────────────────────────── // Main Render // ────────────────────────────────────────────────────────────── override render() { return html` `; } } /** * Props interface for AgTimelineItem component */ export interface AgTimelineItemProps { /** Inherited from parent timeline */ orientation?: 'horizontal' | 'vertical'; /** Inherited from parent timeline */ variant?: 'primary' | 'success' | 'warning' | 'danger' | 'monochrome' | ''; /** ARIA label for the marker icon */ markerAriaLabel?: string; } /** * AgTimelineItem - Individual item within a timeline * * @element ag-timeline-item * * @slot ag-start - Content positioned at the start (e.g., date/label) * @slot ag-marker - Content for the center marker (e.g., icon) * @slot ag-end - Content positioned at the end (e.g., description) * * @csspart ag-item-container - The main item container (li element) * @csspart ag-start - The start content wrapper * @csspart ag-marker - The marker wrapper * @csspart ag-end - The end content wrapper * @csspart ag-connector - The connector line element * * @cssprop --ag-timeline-start-align - Vertical alignment of start content (default: start) * @cssprop --ag-timeline-end-align - Vertical alignment of end content (default: start) */ export class AgTimelineItem extends LitElement { // ────────────────────────────────────────────────────────────── // Public Properties // ────────────────────────────────────────────────────────────── @property({ type: String, reflect: true }) declare orientation: 'horizontal' | 'vertical'; @property({ type: String, reflect: true }) declare variant: 'primary' | 'success' | 'warning' | 'danger' | 'monochrome' | ''; @property({ type: String, attribute: 'marker-aria-label' }) declare markerAriaLabel: string | null; @property({ type: Boolean, reflect: true }) declare first: boolean; @property({ type: Boolean, reflect: true }) declare last: boolean; // ────────────────────────────────────────────────────────────── // Constructor // ────────────────────────────────────────────────────────────── constructor() { super(); this.orientation = 'horizontal'; this.variant = ''; this.first = false; this.last = false; } // ────────────────────────────────────────────────────────────── // Styles // ────────────────────────────────────────────────────────────── static styles = css` :host { display: block; position: relative; } :host([orientation="horizontal"]) { flex: 1; } .item-container { display: grid; position: relative; align-items: center; } /* Horizontal layout - 3x3 grid */ :host([orientation="horizontal"]) .item-container { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); grid-template-rows: auto auto auto; justify-items: center; align-items: center; } /* Vertical layout - 3x3 grid */ /* Vertical layout - Flush alignment */ :host([orientation="vertical"]) .item-container { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); /* Rows: connector-before (0), marker (auto), connector-after + spacing (minmax ensures minimum gap between items) */ grid-template-rows: 0 auto minmax(var(--ag-timeline-vertical-spacing, var(--ag-space-4)), 1fr); align-items: start; justify-items: center; } /* Start slot positioning */ .ag-start { margin: var(--ag-space-1); /* Reset line-height for slotted content */ line-height: normal; } :host([orientation="horizontal"]) .ag-start { grid-column: 1 / 4; grid-row: 1 / 2; align-self: end; justify-self: center; } :host([orientation="vertical"]) .ag-start { grid-column: 1 / 2; grid-row: 2 / 4; align-self: var(--ag-timeline-start-align, start); justify-self: end; margin: 0 var(--ag-space-2) 0 0; } /* Marker slot positioning */ .ag-marker { grid-column: 2 / 3; grid-row: 2 / 3; display: flex; align-items: center; justify-content: center; z-index: 1; /* Reset line-height for slotted content */ line-height: normal; } /* Ensure consistent sizing for slotted marker content */ .ag-marker ::slotted(*) { box-sizing: border-box; } /* End slot positioning */ .ag-end { margin: var(--ag-space-1); /* Reset line-height for slotted content */ line-height: normal; } :host([orientation="horizontal"]) .ag-end { grid-column: 1 / 4; grid-row: 3 / 4; align-self: start; justify-self: center; } :host([orientation="vertical"]) .ag-end { grid-column: 3 / 4; grid-row: 2 / 4; align-self: var(--ag-timeline-end-align, start); justify-self: start; margin: 0 0 0 var(--ag-space-2); } /* Connector lines */ .connector { border: none; background: var(--ag-timeline-connector-color, var(--ag-border)); margin: 0; } .connector-before { grid-column: 1 / 2; grid-row: 2 / 3; } .connector-after { grid-column: 3 / 4; grid-row: 2 / 3; } /* Horizontal connectors */ :host([orientation="horizontal"]) .connector { width: 100%; height: var(--ag-timeline-connector-width, 2px); } :host([orientation="horizontal"][first]) .connector-before { border-radius: var(--ag-radius-full) 0 0 var(--ag-radius-full); } :host([orientation="horizontal"][last]) .connector-after { border-radius: 0 var(--ag-radius-full) var(--ag-radius-full) 0; } /* Vertical connectors */ :host([orientation="vertical"]) .connector { height: 100%; width: var(--ag-timeline-connector-width, 2px); } :host([orientation="vertical"]) .connector-before { grid-column: 2 / 3; grid-row: 1 / 2; } :host([orientation="vertical"]) .connector-after { grid-column: 2 / 3; grid-row: 2 / 4; z-index: 0; } :host([orientation="vertical"][first]) .connector-before { border-radius: var(--ag-radius-full) var(--ag-radius-full) 0 0; } :host([orientation="vertical"][last]) .connector-after { border-radius: 0 0 var(--ag-radius-full) var(--ag-radius-full); } /* Hide first connector on first item */ :host([first]) .connector-before { display: none; } /* Hide last connector on last item */ :host([last]) .connector-after { display: none; } /* Variant colors */ :host([variant="primary"]) .connector { background: var(--ag-primary); } :host([variant="success"]) .connector { background: var(--ag-success); } :host([variant="warning"]) .connector { background: var(--ag-warning); } :host([variant="danger"]) .connector { background: var(--ag-danger); } :host([variant="monochrome"]) .connector { background: var(--ag-text-muted); } `; // ────────────────────────────────────────────────────────────── // Main Render // ────────────────────────────────────────────────────────────── override render() { return html`
  • `; } }