import { css, html } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { BaseElement } from "../../core/base-element.ts"; /** * CTHScroll - Horizontal scroll container * * @element ct-hscroll * * @attr {boolean} show-scrollbar - Always show scrollbar * @attr {boolean} fade-edges - Show fade effect at edges * @attr {string} padding - Padding inside scroll area * * @slot - Content to be scrolled horizontally * * @example * * * Card 1 * Card 2 * Card 3 * * */ export class CTHScroll extends BaseElement { static override properties = { showScrollbar: { type: Boolean, reflect: true, attribute: "show-scrollbar", }, fadeEdges: { type: Boolean, reflect: true, attribute: "fade-edges" }, padding: { type: String }, _atStart: { type: Boolean, state: true }, _atEnd: { type: Boolean, state: true }, }; declare showScrollbar: boolean; declare fadeEdges: boolean; declare padding: string; declare _atStart: boolean; declare _atEnd: boolean; static override styles = css` :host { display: block; position: relative; overflow: hidden; } .scroll-wrapper { position: relative; width: 100%; height: 100%; } .scroll-container { overflow-x: auto; overflow-y: hidden; width: 100%; height: 100%; -webkit-overflow-scrolling: touch; scrollbar-width: thin; } /* Hide scrollbar by default */ :host(:not([show-scrollbar])) .scroll-container { scrollbar-width: none; -ms-overflow-style: none; } :host(:not([show-scrollbar])) .scroll-container::-webkit-scrollbar { display: none; } /* Scrollbar styling when visible */ .scroll-container::-webkit-scrollbar { height: 8px; } .scroll-container::-webkit-scrollbar-track { background: var(--muted, #f1f5f9); border-radius: 4px; } .scroll-container::-webkit-scrollbar-thumb { background: var(--muted-foreground, #64748b); border-radius: 4px; } .scroll-container::-webkit-scrollbar-thumb:hover { background: var(--foreground, #475569); } /* Padding utilities */ .p-0 { padding: 0; } .p-1 { padding: 0.25rem; } .p-2 { padding: 0.5rem; } .p-3 { padding: 0.75rem; } .p-4 { padding: 1rem; } .p-5 { padding: 1.25rem; } .p-6 { padding: 1.5rem; } .p-8 { padding: 2rem; } /* Fade edges */ :host([fade-edges]) .scroll-wrapper::before, :host([fade-edges]) .scroll-wrapper::after { content: ""; position: absolute; top: 0; bottom: 0; width: 2rem; pointer-events: none; z-index: 1; transition: opacity 0.2s; } :host([fade-edges]) .scroll-wrapper::before { left: 0; background: linear-gradient( to right, var(--background, white), transparent ); opacity: 0; } :host([fade-edges]) .scroll-wrapper::after { right: 0; background: linear-gradient(to left, var(--background, white), transparent); opacity: 0; } :host([fade-edges]) .scroll-wrapper:not(.at-start)::before { opacity: 1; } :host([fade-edges]) .scroll-wrapper:not(.at-end)::after { opacity: 1; } /* Ensure content doesn't wrap */ ::slotted(*) { flex-shrink: 0; } `; private _scrollContainer: HTMLElement | null = null; constructor() { super(); this.showScrollbar = false; this.fadeEdges = false; this.padding = "0"; this._atStart = true; this._atEnd = true; } get scrollContainer(): HTMLElement | null { if (!this._scrollContainer) { this._scrollContainer = this.shadowRoot?.querySelector( ".scroll-container", ) as HTMLElement | null; } return this._scrollContainer; } override firstUpdated() { // Cache reference this._scrollContainer = this.shadowRoot?.querySelector( ".scroll-container", ) as HTMLElement | null; this.updateScrollState(); } override connectedCallback() { super.connectedCallback(); // Set up scroll listener after element is ready this.updateComplete.then(() => { if (this.scrollContainer) { (this.scrollContainer as HTMLElement).addEventListener( "scroll", this.handleScroll, ); } }); } override disconnectedCallback() { super.disconnectedCallback(); if (this.scrollContainer) { (this.scrollContainer as HTMLElement).removeEventListener( "scroll", this.handleScroll, ); } } private handleScroll = () => { this.updateScrollState(); this.emit("ct-scroll", { scrollLeft: (this.scrollContainer as HTMLElement)?.scrollLeft || 0, scrollWidth: (this.scrollContainer as HTMLElement)?.scrollWidth || 0, clientWidth: (this.scrollContainer as HTMLElement)?.clientWidth || 0, }); }; private updateScrollState() { if (!this.scrollContainer || !this.fadeEdges) return; const { scrollLeft, scrollWidth, clientWidth } = this .scrollContainer as HTMLElement; this._atStart = scrollLeft <= 0; this._atEnd = scrollLeft + clientWidth >= scrollWidth - 1; } override render() { const wrapperClasses = { "scroll-wrapper": true, "at-start": this._atStart, "at-end": this._atEnd, }; const containerClasses = { "scroll-container": true, [`p-${this.padding}`]: true, }; return html`
`; } /** * Scroll to a specific horizontal position */ scrollToX(x: number, smooth: boolean = true) { if (this.scrollContainer) { (this.scrollContainer as HTMLElement).scrollTo({ left: x, behavior: smooth ? "smooth" : "auto", }); } } /** * Scroll by a specific horizontal amount */ scrollByX(x: number, smooth: boolean = true) { if (this.scrollContainer) { (this.scrollContainer as HTMLElement).scrollBy({ left: x, behavior: smooth ? "smooth" : "auto", }); } } } globalThis.customElements.define("ct-hscroll", CTHScroll);