forked from
grain.social/grain-pwa
WIP PWA for Grain
1import { LitElement, html, css } from 'lit';
2
3export class GrainCarouselDots extends LitElement {
4 static properties = {
5 total: { type: Number },
6 current: { type: Number }
7 };
8
9 static styles = css`
10 :host {
11 display: flex;
12 justify-content: center;
13 align-items: center;
14 gap: 4px;
15 padding: var(--space-sm) 0;
16 }
17 .dot {
18 width: 6px;
19 height: 6px;
20 border-radius: 50%;
21 background: var(--color-text-secondary);
22 opacity: 0.4;
23 transition: opacity 0.2s, transform 0.2s;
24 }
25 .dot.active {
26 opacity: 1;
27 background: white;
28 }
29 .dot.small {
30 width: 4px;
31 height: 4px;
32 opacity: 0.3;
33 }
34 .dot.tiny {
35 width: 3px;
36 height: 3px;
37 opacity: 0.2;
38 }
39 `;
40
41 constructor() {
42 super();
43 this.total = 0;
44 this.current = 0;
45 }
46
47 #getDotClass(index) {
48 const distance = Math.abs(index - this.current);
49 if (index === this.current) return 'dot active';
50 if (distance === 1) return 'dot';
51 if (distance === 2) return 'dot small';
52 return 'dot tiny';
53 }
54
55 render() {
56 if (this.total <= 1) return null;
57
58 // For 5 or fewer, show all dots
59 if (this.total <= 5) {
60 return html`
61 ${Array.from({ length: this.total }, (_, i) => html`
62 <div class="${this.#getDotClass(i)}"></div>
63 `)}
64 `;
65 }
66
67 // For more than 5, use sliding window of 5 dots centered on current
68 const maxVisible = 5;
69 let start = Math.max(0, this.current - 2);
70 let end = start + maxVisible;
71
72 if (end > this.total) {
73 end = this.total;
74 start = end - maxVisible;
75 }
76
77 return html`
78 ${Array.from({ length: end - start }, (_, i) => {
79 const index = start + i;
80 return html`<div class="${this.#getDotClass(index)}"></div>`;
81 })}
82 `;
83 }
84}
85
86customElements.define('grain-carousel-dots', GrainCarouselDots);