coverflow-carousel 0.1.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ux-ui.pro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ <p align="center"><strong>coverflow-carousel</strong></p>
2
+
3
+ <div align="center">
4
+ <p align="center">CoverflowCarousel is a tiny “coverflow”-style Web Component carousel. It registers <code>&lt;coverflow-carousel&gt;</code>, renders its children as a 3D card stack, is controlled via attributes, and dispatches events on ready and slide change.</p>
5
+
6
+ [![npm](https://img.shields.io/npm/v/coverflow-carousel.svg?colorB=brightgreen)](https://www.npmjs.com/package/coverflow-carousel)
7
+ [![GitHub package version](https://img.shields.io/github/package-json/v/ux-ui-pro/coverflow-carousel.svg)](https://github.com/ux-ui-pro/coverflow-carousel)
8
+ [![NPM Downloads](https://img.shields.io/npm/dm/coverflow-carousel.svg?style=flat)](https://www.npmjs.org/package/coverflow-carousel)
9
+
10
+ <a href="https://codepen.io/ux-ui/pen/qENrydj">Demo</a>
11
+ </div>
12
+ <br>
13
+
14
+ ➠ **Install**
15
+
16
+ ```console
17
+ yarn add coverflow-carousel
18
+ ```
19
+ <br>
20
+
21
+ ➠ **Import**
22
+
23
+ ```javascript
24
+ // Registers <coverflow-carousel> via customElements.define(...)
25
+ import 'coverflow-carousel';
26
+
27
+ // Optional helper: subscribe to events + apply styles
28
+ import { initCoverflowCarousels } from 'coverflow-carousel';
29
+ ```
30
+ <br>
31
+
32
+ ➠ **Usage**
33
+
34
+ ```javascript
35
+ // After import 'coverflow-carousel', the element is registered.
36
+ // Then just use <coverflow-carousel> in your HTML (see below).
37
+ ```
38
+
39
+ <sub>HTML: basic example</sub>
40
+ ```html
41
+ <coverflow-carousel
42
+ start-index="0"
43
+ aria-label="Coverflow carousel"
44
+ >
45
+ <img src="/images/1.jpg" alt="" />
46
+ <img src="/images/2.jpg" alt="" />
47
+ <img src="/images/3.jpg" alt="" />
48
+ <img src="/images/4.jpg" alt="" />
49
+ <img src="/images/5.jpg" alt="" />
50
+ </coverflow-carousel>
51
+ ```
52
+
53
+ <sub>JS: subscribe to events + optional styles</sub>
54
+ ```javascript
55
+ import { initCoverflowCarousels, coverflowCarouselCssText } from 'coverflow-carousel';
56
+
57
+ initCoverflowCarousels({
58
+ selector: 'coverflow-carousel',
59
+ onReady: (el, detail) => {
60
+ // detail: { index, length }
61
+ el.classList.add('is-ready');
62
+ console.log('ready', detail);
63
+ },
64
+ onChange: (_el, detail) => {
65
+ console.log('change', detail);
66
+ },
67
+ onScratchComplete: (el, detail) => {
68
+ // detail: { index, length, percent }
69
+ console.log('scratch complete', detail);
70
+
71
+ // Example sequence: auto-advance first 2, then finish on 3rd.
72
+ if (detail.index === 0) (el as any).goTo?.(1);
73
+ else if (detail.index === 1) (el as any).goTo?.(2);
74
+ else if (detail.index === 2) console.log('final');
75
+ },
76
+ // Optional styles:
77
+ // - CSSStyleSheet (constructable stylesheet)
78
+ // - string (e.g. imported via ?raw)
79
+ // stylesheet: coverflowCarouselCssText,
80
+ });
81
+ ```
82
+
83
+ <sub>JS: custom styles from `?raw` (CSS/SCSS)</sub>
84
+ ```javascript
85
+ import { initCoverflowCarousels } from 'coverflow-carousel';
86
+ import MyCfcCss from './assets/cfc.custom.css?raw';
87
+
88
+ initCoverflowCarousels({
89
+ stylesheet: MyCfcCss,
90
+ });
91
+ ```
92
+
93
+ <sub>JS: default styles shipped with the package</sub>
94
+ ```javascript
95
+ import { initCoverflowCarousels, coverflowCarouselCssText } from 'coverflow-carousel';
96
+
97
+ initCoverflowCarousels({
98
+ stylesheet: coverflowCarouselCssText,
99
+ });
100
+ ```
101
+
102
+ <sub>JS: manual control (prev/next/goTo/refresh)</sub>
103
+ ```javascript
104
+ const el = document.querySelector('coverflow-carousel');
105
+ // @ts-expect-error: methods exist on the custom element instance after import
106
+ el?.prev();
107
+ // @ts-expect-error
108
+ el?.next();
109
+ // @ts-expect-error
110
+ el?.goTo(2);
111
+ // @ts-expect-error: if you added/removed slides dynamically — call refresh()
112
+ el?.refresh();
113
+ ```
114
+ <br>
115
+
116
+ ➠ **Options**
117
+
118
+ | Option (attribute) | Type | Default | Description |
119
+ |:--------------------:|:-----------------------:|:------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
120
+ | `start-index` | `number` | `0` | Start index if `index` is not set. |
121
+ | `index` | `number` | — | Current index. If you update it externally, the carousel will animate to it. The component also reflects the current value back into `index`. |
122
+ | `show-dots` | `boolean` | `false` | Shows pagination dots (decorative, non-interactive). |
123
+ | `show-arrows` | `boolean` | `false` | Shows Prev/Next arrows. |
124
+ | `announce-changes` | `boolean` | `true` | Enables slide-change announcements via live-region (a11y). |
125
+
126
+ <br>
127
+
128
+ ➠ **Events**
129
+
130
+ | Event | Detail | Description |
131
+ |-------------------|------------------------------|--------------------------------------------------------------------------------------------------|
132
+ | `coverflow-carousel:ready` | `{ index: number, length: number }` | Fired after the first `refresh()` (when slides are built and layout/a11y is applied). |
133
+ | `coverflow-carousel:change` | `{ index: number, length: number }` | Fired on active slide change via `prev/next/goTo` (and when `index` is updated externally). |
134
+ | `coverflow-carousel:scratch-complete` | `{ index: number, length: number, percent: number }` | Fired when a descendant `<scratch-reveal>` dispatches `complete` for a slide. |
135
+
136
+ <br>
137
+
138
+ ➠ **API Methods**
139
+
140
+ | Method | Description |
141
+ |-------------------|--------------------------------------------------------------------------------------------------|
142
+ | `initCoverflowCarousels({ selector?, onReady?, onChange?, onScratchComplete?, stylesheet? }): HTMLElement[]` | Finds elements by `selector` (default: `coverflow-carousel`), optionally subscribes to `ready/change/scratch-complete` events, and optionally applies styles to each element (`stylesheet?: CSSStyleSheet \| string \| null`). |
143
+ | `prev(): void` | Go to the previous slide (circular). |
144
+ | `next(): void` | Go to the next slide (circular). |
145
+ | `goTo(index: number): void` | Go to the given index (circular normalization). While animating, repeated transitions are ignored. |
146
+ | `refresh(): void` | Rebuilds cards from current `children` (useful after dynamic changes) and dispatches `coverflow-carousel:ready`. |
147
+ | `destroy(): void` | Removes event handlers and cancels animation-related timers. |
148
+ | `adoptStylesheet(sheet: CSSStyleSheet): void` | Applies a stylesheet via `adoptedStyleSheets` (when supported). |
149
+ | `adoptStyles(styles: CSSStyleSheet \| string \| null): void` | Applies styles: string via `adoptedStyleSheets` (when possible) or `<style>` fallback in the shadow root; `null` restores the package default styles. |
150
+
151
+ <br>
152
+
153
+ ➠ **Notes**
154
+
155
+ - Slides come from *light DOM*: all children are moved into cards inside the shadow DOM (nodes are moved — this is expected).
156
+ - For the coverflow effect, it’s recommended to have **at least 3** slides.
157
+ - While a transition is running, new transitions are ignored (anti-spam guard).
158
+ - `prefers-reduced-motion` is respected: when enabled, the animation lock is released without waiting for `transitionend`.
159
+ - The carousel always shows **3 visible cards** (center + neighbors).
160
+ - By default, arrows and dots are hidden. Add `show-arrows` / `show-dots` attributes to show them.
161
+ - Transition speed and easing are controlled via CSS variables `--cfc-transition-ms` and `--cfc-easing`.
162
+
163
+ <br>
164
+
165
+ ➠ **License**
166
+
167
+ coverflow-carousel is released under MIT license
@@ -0,0 +1 @@
1
+ :host{width:100%;height:100%;display:block;overflow:hidden;container-type:inline-size}.cfc{--cfc-card-ar-num:1.25;--cfc-card-w:clamp(160px,100vw - 120px,380px);--cfc-card-ar:1/var(--cfc-card-ar-num);--cfc-step-x:min(22vw,250px);--cfc-step-z:calc(var(--cfc-card-w)*.5);--cfc-scale-step:.1;--cfc-transition-ms:.55s;--cfc-easing:cubic-bezier(.785,.135,.15,.86);--cfc-arrow-size:50px;--cfc-park-x:0px;--cfc-park-z:calc(var(--cfc-step-z)*-6);--cfc-park-scale:.1;perspective:1000px;width:min(900px,100%);height:100%;margin:auto;position:relative}.cfc__track{width:100%;height:calc(var(--cfc-card-w)*var(--cfc-card-ar-num));transform-style:preserve-3d;position:relative}.cfc__card{--cfc-delta:0;--cfc-abs:0;--cfc-scale:clamp(.1,calc(1 - (var(--cfc-abs)*var(--cfc-scale-step))),1);--cfc-x:calc(var(--cfc-step-x)*var(--cfc-delta));--cfc-z:calc(var(--cfc-step-z)*-1*var(--cfc-abs));--cfc-zIndex:calc(100 - var(--cfc-abs));--cfc-scale-effective:var(--cfc-scale);--cfc-x-effective:var(--cfc-x);--cfc-z-effective:var(--cfc-z);pointer-events:auto;width:var(--cfc-card-w);aspect-ratio:var(--cfc-card-ar);z-index:var(--cfc-zIndex);transform-style:preserve-3d;transform:translate(-50%,-50%)translate3d(var(--cfc-x-effective),0px,var(--cfc-z-effective))scale(var(--cfc-scale-effective));transition:transform var(--cfc-transition-ms)var(--cfc-easing);position:absolute;inset:50% 0 0 50%;overflow:hidden}.cfc__card[aria-hidden=false]{will-change:transform}.cfc__card[aria-hidden=true]{--cfc-x-effective:var(--cfc-park-x);--cfc-z-effective:var(--cfc-park-z);--cfc-scale-effective:var(--cfc-park-scale);pointer-events:none}.cfc__card[data-active=true]{--cfc-scale-effective:1}.cfc__card ::slotted(img),.cfc__card img{object-fit:cover;object-position:center;width:100%;height:100%;display:block}.cfc__arrow{top:calc(50% - var(--cfc-arrow-size));width:var(--cfc-arrow-size);height:var(--cfc-arrow-size);cursor:pointer;z-index:50;background:#dc143c;border:none;transition:background .25s;position:absolute}.cfc__arrow:hover{background:#d3d3d3}.cfc__arrow--left{left:20px}.cfc__arrow--right{right:20px}.cfc__dots{justify-content:center;align-items:center;gap:10px;height:50px;display:flex}.cfc__dot{cursor:default;background:#d3d3d3;border-radius:50%;width:10px;height:10px;transition:transform .25s,background .25s}.cfc__dot[data-active=true]{background:#dc143c;transform:scale(1.2)}.cfc__sr{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}
@@ -0,0 +1,52 @@
1
+ import { StylesInput } from '../styles';
2
+ export declare class CoverflowCarouselElement extends HTMLElement {
3
+ static defaultStylesheet: CSSStyleSheet | null;
4
+ static observedAttributes: string[];
5
+ private readonly shadow;
6
+ private readonly instanceId;
7
+ private rootEl;
8
+ private trackEl;
9
+ private dotsEl;
10
+ private prevBtn;
11
+ private nextBtn;
12
+ private liveRegion;
13
+ private styleEl;
14
+ private cards;
15
+ private dots;
16
+ private currentIndex;
17
+ private isAnimating;
18
+ private hasAppliedInitialLayout;
19
+ private lastLayoutIndex;
20
+ private lastVisibleSet;
21
+ private pendingAnimToken;
22
+ private animFallbackTimerId;
23
+ private cleanup;
24
+ private reflectGuard;
25
+ connectedCallback(): void;
26
+ disconnectedCallback(): void;
27
+ attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null): void;
28
+ next(): void;
29
+ prev(): void;
30
+ goTo(index: number): void;
31
+ refresh(): void;
32
+ destroy(): void;
33
+ adoptStylesheet(sheet: CSSStyleSheet): void;
34
+ adoptStyles(styles: StylesInput): void;
35
+ private readAttributes;
36
+ private render;
37
+ private bindEvents;
38
+ private rebuildCardsFromLightDom;
39
+ private getVisibleSet;
40
+ private buildDots;
41
+ private computeCardState;
42
+ private applyCardState;
43
+ private applyLayoutAndA11y;
44
+ private updateDotsVisualState;
45
+ private lockUntilTransitionEnd;
46
+ private unlockAnimation;
47
+ private emitChange;
48
+ private dispatchReady;
49
+ private announce;
50
+ private reflectIndexAttr;
51
+ private applyStyles;
52
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const v=`:host {
2
+ display: block;
3
+ width: 100%;
4
+ height: 100%;
5
+ container-type: inline-size;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .cfc {
10
+ /* Layout */
11
+ --cfc-card-ar-num: 1.25;
12
+ --cfc-card-w: clamp(160px, 100vw - 120px, 380px);
13
+ --cfc-card-ar: 1 / var(--cfc-card-ar-num);
14
+
15
+ /* Motion / depth */
16
+ --cfc-step-x: min(22vw, 250px);
17
+ --cfc-step-z: calc(var(--cfc-card-w) * 0.5);
18
+
19
+ /* Visual falloff */
20
+ --cfc-scale-step: 0.1;
21
+
22
+ /* Transitions */
23
+ --cfc-transition-ms: 550ms;
24
+ --cfc-easing: cubic-bezier(0.785, 0.135, 0.15, 0.86);
25
+
26
+ /* Controls */
27
+ --cfc-arrow-size: 50px;
28
+
29
+ /* CSS-only "parking strategy" for hidden cards */
30
+ --cfc-park-x: 0px;
31
+ --cfc-park-z: calc(var(--cfc-step-z) * -6);
32
+ --cfc-park-scale: 0.1;
33
+
34
+ margin: auto;
35
+ width: min(900px, 100%);
36
+ height: 100%;
37
+ position: relative;
38
+ perspective: 1000px;
39
+ }
40
+
41
+ .cfc__track {
42
+ width: 100%;
43
+ height: calc(var(--cfc-card-w) * var(--cfc-card-ar-num));
44
+ position: relative;
45
+ transform-style: preserve-3d;
46
+ }
47
+
48
+ .cfc__card {
49
+ /* Inputs from JS */
50
+ --cfc-delta: 0;
51
+ --cfc-abs: 0;
52
+
53
+ /* Derived values */
54
+ --cfc-scale: clamp(0.1, calc(1 - (var(--cfc-abs) * var(--cfc-scale-step))), 1);
55
+ --cfc-x: calc(var(--cfc-step-x) * var(--cfc-delta));
56
+ --cfc-z: calc(var(--cfc-step-z) * -1 * var(--cfc-abs));
57
+ --cfc-zIndex: calc(100 - var(--cfc-abs));
58
+
59
+ /* Effective values (can be overridden when hidden/active) */
60
+ --cfc-scale-effective: var(--cfc-scale);
61
+ --cfc-x-effective: var(--cfc-x);
62
+ --cfc-z-effective: var(--cfc-z);
63
+
64
+ pointer-events: auto;
65
+
66
+ position: absolute;
67
+ inset: 50% 0 0 50%;
68
+ width: var(--cfc-card-w);
69
+ aspect-ratio: var(--cfc-card-ar);
70
+ overflow: hidden;
71
+
72
+ z-index: var(--cfc-zIndex);
73
+
74
+ transform-style: preserve-3d;
75
+ transform: translate(-50%, -50%) translate3d(var(--cfc-x-effective), 0px, var(--cfc-z-effective))
76
+ scale(var(--cfc-scale-effective));
77
+
78
+ transition: transform var(--cfc-transition-ms) var(--cfc-easing);
79
+ }
80
+
81
+ .cfc__card[aria-hidden="false"] {
82
+ will-change: transform;
83
+ }
84
+
85
+ .cfc__card[aria-hidden="true"] {
86
+ /* CSS-only parking strategy */
87
+ --cfc-x-effective: var(--cfc-park-x);
88
+ --cfc-z-effective: var(--cfc-park-z);
89
+ --cfc-scale-effective: var(--cfc-park-scale);
90
+
91
+ pointer-events: none;
92
+ }
93
+
94
+ .cfc__card[data-active="true"] {
95
+ --cfc-scale-effective: 1;
96
+ }
97
+
98
+ .cfc__card ::slotted(img),
99
+ .cfc__card img {
100
+ width: 100%;
101
+ height: 100%;
102
+ display: block;
103
+ object-fit: cover;
104
+ object-position: center;
105
+ }
106
+
107
+ .cfc__arrow {
108
+ position: absolute;
109
+ top: calc(50% - var(--cfc-arrow-size));
110
+ width: var(--cfc-arrow-size);
111
+ height: var(--cfc-arrow-size);
112
+ border: none;
113
+ background: Crimson;
114
+ cursor: pointer;
115
+ z-index: 50;
116
+ transition: background 0.25s ease;
117
+ }
118
+
119
+ .cfc__arrow:hover {
120
+ background: LightGray;
121
+ }
122
+
123
+ .cfc__arrow--left {
124
+ left: 20px;
125
+ }
126
+
127
+ .cfc__arrow--right {
128
+ right: 20px;
129
+ }
130
+
131
+ .cfc__dots {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ gap: 10px;
136
+ height: 50px;
137
+ }
138
+
139
+ .cfc__dot {
140
+ width: 10px;
141
+ height: 10px;
142
+ border-radius: 50%;
143
+ background: LightGray;
144
+ cursor: default;
145
+ transition:
146
+ transform 0.25s ease,
147
+ background 0.25s ease;
148
+ }
149
+
150
+ .cfc__dot[data-active="true"] {
151
+ background: Crimson;
152
+ transform: scale(1.2);
153
+ }
154
+
155
+ /* SR-only */
156
+ .cfc__sr {
157
+ position: absolute;
158
+ width: 1px;
159
+ height: 1px;
160
+ padding: 0;
161
+ margin: -1px;
162
+ overflow: hidden;
163
+ clip: rect(0, 0, 0, 0);
164
+ white-space: nowrap;
165
+ border: 0;
166
+ }
167
+ `,S=v;function f(a){return"adoptedStyleSheets"in a}function b(a){try{const t=new CSSStyleSheet;return t.replaceSync(a),t}catch{return null}}const I=b(v),_=v;function p(a){return a.split(",").map(t=>t.trim()).filter(Boolean)}function m(a,t){const e=a.trim();if(!e)return t;if(e.endsWith("ms")){const n=Number(e.slice(0,-2).trim());return Number.isFinite(n)?n:t}if(e.endsWith("s")){const n=Number(e.slice(0,-1).trim());return Number.isFinite(n)?n*1e3:t}const i=Number(e);return Number.isFinite(i)?i:t}function k(a,t){const e=p(a.transitionProperty),i=p(a.transitionDuration),n=p(a.transitionDelay);if(!e.length)return 0;const r=(A=>{const x=e.indexOf(A);if(x>=0)return x;const g=e.indexOf("all");return g>=0?g:-1})(t);if(r<0)return 0;const c=i[Math.min(r,i.length-1)]??"0s",o=n[Math.min(r,n.length-1)]??"0s",w=m(c,0),E=m(o,0);return Math.max(0,w+E)}function C(){return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches??!1}function l(a,t,e){if(!a.hasAttribute(t))return e;const i=a.getAttribute(t);return i==null||i===""?!0:i!=="false"}function T(a,t,e){const i=a.getAttribute(t);if(i==null)return e;const n=Number(i.trim());return Number.isFinite(n)?Math.trunc(n):e}function y(a,t){const e=a.getAttribute(t);if(e==null)return null;const i=e.trim();return i||null}function d(a,t){return t<=0?0:(a%t+t)%t}function L(a,t,e){const i=t-a,n=e/2;return i>n?i-e:i<-n?i+e:i}const h=1;let D=0;class u extends HTMLElement{static defaultStylesheet=I;static observedAttributes=["start-index","index","show-dots","show-arrows","announce-changes"];shadow=this.attachShadow({mode:"open"});instanceId=`cfc-${++D}`;rootEl;trackEl;dotsEl;prevBtn;nextBtn;liveRegion;styleEl=null;cards=[];dots=[];currentIndex=0;isAnimating=!1;hasAppliedInitialLayout=!1;lastLayoutIndex=null;lastVisibleSet=new Set;pendingAnimToken=0;animFallbackTimerId=null;cleanup=[];reflectGuard=!1;connectedCallback(){this.render(),this.readAttributes({isInit:!0}),this.refresh()}disconnectedCallback(){this.destroy()}attributeChangedCallback(t,e,i){if(!this.isConnected||this.reflectGuard)return;this.readAttributes({isInit:!1});const n=y(this,"index");if(n!=null){const s=d(Number(n),this.cards.length);if(Number.isFinite(s)&&s!==this.currentIndex){this.goTo(s);return}}this.applyLayoutAndA11y({announce:!0,emitChange:!1})}next(){this.goTo(this.currentIndex+1)}prev(){this.goTo(this.currentIndex-1)}goTo(t){if(this.isAnimating)return;const e=d(t,this.cards.length);e!==this.currentIndex&&(this.isAnimating=!0,this.currentIndex=e,this.reflectIndexAttr(),this.applyLayoutAndA11y({announce:!0,emitChange:!0}),this.lockUntilTransitionEnd())}refresh(){this.rebuildCardsFromLightDom();const t=T(this,"start-index",0),e=y(this,"index"),i=e!=null?Number(e):t,n=Number.isFinite(i)?i:0;this.currentIndex=d(n,this.cards.length),this.reflectIndexAttr(),this.buildDots(),this.hasAppliedInitialLayout=!1,this.lastLayoutIndex=null,this.lastVisibleSet=new Set,this.applyLayoutAndA11y({announce:!0,emitChange:!1}),this.dispatchReady()}destroy(){this.animFallbackTimerId!==null&&(window.clearTimeout(this.animFallbackTimerId),this.animFallbackTimerId=null),this.cleanup.forEach(t=>{t()}),this.cleanup=[],this.isAnimating=!1}adoptStylesheet(t){this.applyStyles(t)}adoptStyles(t){this.applyStyles(t)}readAttributes(t){this.rootEl||this.render();const e=l(this,"show-arrows",!1),i=l(this,"show-dots",!1);this.prevBtn.style.display=e?"":"none",this.nextBtn.style.display=e?"":"none",this.dotsEl.style.display=i?"":"none";const n=this.getAttribute("aria-label");n&&this.rootEl.setAttribute("aria-label",n),t.isInit&&this.setAttribute("aria-roledescription","carousel")}render(){this.applyStyles(null),this.rootEl=document.createElement("div"),this.rootEl.className="cfc",this.trackEl=document.createElement("div"),this.trackEl.className="cfc__track",this.prevBtn=document.createElement("button"),this.prevBtn.type="button",this.prevBtn.className="cfc__arrow cfc__arrow--left",this.prevBtn.setAttribute("aria-label","Previous"),this.prevBtn.textContent="‹",this.nextBtn=document.createElement("button"),this.nextBtn.type="button",this.nextBtn.className="cfc__arrow cfc__arrow--right",this.nextBtn.setAttribute("aria-label","Next"),this.nextBtn.textContent="›",this.dotsEl=document.createElement("div"),this.dotsEl.className="cfc__dots",this.liveRegion=document.createElement("div"),this.liveRegion.className="cfc__sr",this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("aria-atomic","true"),this.rootEl.append(this.trackEl,this.prevBtn,this.nextBtn,this.dotsEl,this.liveRegion),this.shadow.innerHTML="",this.styleEl&&this.shadow.append(this.styleEl),this.shadow.append(this.rootEl),this.bindEvents()}bindEvents(){this.cleanup.forEach(n=>{n()}),this.cleanup=[];const t=()=>this.prev(),e=()=>this.next();this.prevBtn.addEventListener("click",t),this.nextBtn.addEventListener("click",e),this.cleanup.push(()=>this.prevBtn.removeEventListener("click",t)),this.cleanup.push(()=>this.nextBtn.removeEventListener("click",e));const i=n=>{if(!this.isAnimating||n.propertyName!=="transform"||!(n.target instanceof HTMLElement))return;const s=this.cards[this.currentIndex];s&&n.target===s&&this.unlockAnimation()};this.rootEl.addEventListener("transitionend",i),this.cleanup.push(()=>this.rootEl.removeEventListener("transitionend",i))}rebuildCardsFromLightDom(){const t=Array.from(this.trackEl.children).filter(r=>r instanceof HTMLElement&&r.classList.contains("cfc__card")),e=[];t.forEach(r=>{const c=r.firstElementChild;c instanceof HTMLElement&&e.push(c)});const i=Array.from(this.children).filter(r=>r instanceof HTMLElement),n=[...e,...i],s=[];for(let r=0;r<n.length;r++){const c=n[r],o=t[r]??document.createElement("div");t[r]||(o.className="cfc__card",this.trackEl.append(o)),o.firstElementChild!==c&&o.replaceChildren(c),s.push(o)}for(let r=n.length;r<t.length;r++)t[r]?.remove();this.cards=s,this.hasAppliedInitialLayout=!1,this.lastLayoutIndex=null,this.lastVisibleSet=new Set}getVisibleSet(){const t=this.cards.length,e=new Set;if(t<=0)return e;const i=Math.floor(t/2);if(h>=i){for(let n=0;n<t;n++)e.add(n);return e}for(let n=-h;n<=h;n++)e.add(d(this.currentIndex+n,t));return e}buildDots(){const t=Array.from(this.dotsEl.children).filter(s=>s instanceof HTMLElement&&s.classList.contains("cfc__dot"));if(!l(this,"show-dots",!1)){t.forEach(s=>{s.remove()}),this.dots=[];return}const i=[],n=this.cards.length;for(let s=0;s<n;s++){const r=t[s]??document.createElement("span");t[s]||this.dotsEl.append(r),r.className="cfc__dot",r.setAttribute("aria-hidden","true"),i.push(r)}for(let s=n;s<t.length;s++)t[s]?.remove();this.dots=i,this.updateDotsVisualState()}computeCardState(t){const e=this.cards.length,i=L(this.currentIndex,t,e),n=Math.abs(i);return{index:t,delta:i,abs:n,isVisible:n<=h,isActive:t===this.currentIndex}}applyCardState(t,e){t.setAttribute("aria-hidden",e.isVisible?"false":"true"),t.dataset.active=e.isActive?"true":"false",t.setAttribute("role","group"),t.setAttribute("aria-roledescription","slide"),t.setAttribute("aria-setsize",String(this.cards.length)),t.setAttribute("aria-posinset",String(e.index+1));const i=`${this.instanceId}-slide-${e.index}`;t.id=i;const n=String(e.delta),s=String(e.abs),r=t.dataset.cfcDelta,c=t.dataset.cfcAbs;r!==n&&(t.style.setProperty("--cfc-delta",n),t.dataset.cfcDelta=n),c!==s&&(t.style.setProperty("--cfc-abs",s),t.dataset.cfcAbs=s)}applyLayoutAndA11y(t){if(this.cards.length){if(this.hasAppliedInitialLayout){const e=this.getVisibleSet(),i=new Set;this.lastVisibleSet.forEach(n=>{i.add(n)}),e.forEach(n=>{i.add(n)}),this.lastLayoutIndex!=null&&i.add(this.lastLayoutIndex),i.add(this.currentIndex),i.forEach(n=>{const s=this.cards[n];if(!s)return;const r=this.computeCardState(n);this.applyCardState(s,r)}),this.lastLayoutIndex=this.currentIndex,this.lastVisibleSet=e}else{for(let e=0;e<this.cards.length;e++){const i=this.cards[e];if(!i)continue;const n=this.computeCardState(e);this.applyCardState(i,n)}this.hasAppliedInitialLayout=!0,this.lastLayoutIndex=this.currentIndex,this.lastVisibleSet=this.getVisibleSet()}this.updateDotsVisualState(),t.announce&&l(this,"announce-changes",!0)&&this.announce(`Slide ${this.currentIndex+1} of ${this.cards.length}`),t.emitChange&&this.emitChange()}}updateDotsVisualState(){if(this.dots.length)for(let t=0;t<this.dots.length;t++){const e=this.dots[t],i=t===this.currentIndex;e.dataset.active=i?"true":"false"}}lockUntilTransitionEnd(){this.pendingAnimToken++;const t=this.pendingAnimToken;if(C()){this.unlockAnimation();return}const e=this.cards[this.currentIndex];if(!e){this.unlockAnimation();return}const i=getComputedStyle(e),n=k(i,"transform");if(n<=0){this.unlockAnimation();return}const s=m(i.getPropertyValue("--cfc-transition-ms").trim(),400),c=Math.max(n,s)+60;this.animFallbackTimerId!==null&&window.clearTimeout(this.animFallbackTimerId),this.animFallbackTimerId=window.setTimeout(()=>{this.pendingAnimToken===t&&this.unlockAnimation()},c)}unlockAnimation(){this.isAnimating=!1,this.animFallbackTimerId!==null&&(window.clearTimeout(this.animFallbackTimerId),this.animFallbackTimerId=null)}emitChange(){this.dispatchEvent(new CustomEvent("coverflow-carousel:change",{detail:{index:this.currentIndex,length:this.cards.length}}))}dispatchReady(){this.dispatchEvent(new CustomEvent("coverflow-carousel:ready",{detail:{index:this.currentIndex,length:this.cards.length}}))}announce(t){this.liveRegion.textContent="",queueMicrotask(()=>{this.liveRegion.textContent=t})}reflectIndexAttr(){this.reflectGuard=!0;try{this.setAttribute("index",String(this.currentIndex))}finally{this.reflectGuard=!1}}applyStyles(t){if(typeof t=="string"){if(f(this.shadow)){const e=b(t);if(e){this.shadow.adoptedStyleSheets=[e],this.styleEl=null;return}}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=t;return}if(t&&f(this.shadow)){this.shadow.adoptedStyleSheets=[t],this.styleEl=null;return}if(u.defaultStylesheet&&f(this.shadow)){this.shadow.adoptedStyleSheets=[u.defaultStylesheet],this.styleEl=null;return}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=_}}function N(a={}){const{selector:t="coverflow-carousel",onReady:e,onChange:i,stylesheet:n}=a,s=Array.from(document.querySelectorAll(t));return(e||i)&&s.forEach(r=>{e&&r.addEventListener("coverflow-carousel:ready",c=>{e(r,c.detail)}),i&&r.addEventListener("coverflow-carousel:change",c=>{i(r,c.detail)})}),n&&s.forEach(r=>{const c=r;typeof n=="string"?c.adoptStyles(n):c.adoptStylesheet(n)}),s}customElements.get("coverflow-carousel")||customElements.define("coverflow-carousel",u);exports.coverflowCarouselCssText=S;exports.initCoverflowCarousels=N;
@@ -0,0 +1,9 @@
1
+ import { CoverflowCarouselElement } from './element/CoverflowCarouselElement';
2
+ import { initCoverflowCarousels } from './init';
3
+ import { coverflowCarouselCssText } from './styles';
4
+ declare global {
5
+ interface HTMLElementTagNameMap {
6
+ 'coverflow-carousel': CoverflowCarouselElement;
7
+ }
8
+ }
9
+ export { coverflowCarouselCssText, initCoverflowCarousels };
@@ -0,0 +1,529 @@
1
+ const v = `:host {
2
+ display: block;
3
+ width: 100%;
4
+ height: 100%;
5
+ container-type: inline-size;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .cfc {
10
+ /* Layout */
11
+ --cfc-card-ar-num: 1.25;
12
+ --cfc-card-w: clamp(160px, 100vw - 120px, 380px);
13
+ --cfc-card-ar: 1 / var(--cfc-card-ar-num);
14
+
15
+ /* Motion / depth */
16
+ --cfc-step-x: min(22vw, 250px);
17
+ --cfc-step-z: calc(var(--cfc-card-w) * 0.5);
18
+
19
+ /* Visual falloff */
20
+ --cfc-scale-step: 0.1;
21
+
22
+ /* Transitions */
23
+ --cfc-transition-ms: 550ms;
24
+ --cfc-easing: cubic-bezier(0.785, 0.135, 0.15, 0.86);
25
+
26
+ /* Controls */
27
+ --cfc-arrow-size: 50px;
28
+
29
+ /* CSS-only "parking strategy" for hidden cards */
30
+ --cfc-park-x: 0px;
31
+ --cfc-park-z: calc(var(--cfc-step-z) * -6);
32
+ --cfc-park-scale: 0.1;
33
+
34
+ margin: auto;
35
+ width: min(900px, 100%);
36
+ height: 100%;
37
+ position: relative;
38
+ perspective: 1000px;
39
+ }
40
+
41
+ .cfc__track {
42
+ width: 100%;
43
+ height: calc(var(--cfc-card-w) * var(--cfc-card-ar-num));
44
+ position: relative;
45
+ transform-style: preserve-3d;
46
+ }
47
+
48
+ .cfc__card {
49
+ /* Inputs from JS */
50
+ --cfc-delta: 0;
51
+ --cfc-abs: 0;
52
+
53
+ /* Derived values */
54
+ --cfc-scale: clamp(0.1, calc(1 - (var(--cfc-abs) * var(--cfc-scale-step))), 1);
55
+ --cfc-x: calc(var(--cfc-step-x) * var(--cfc-delta));
56
+ --cfc-z: calc(var(--cfc-step-z) * -1 * var(--cfc-abs));
57
+ --cfc-zIndex: calc(100 - var(--cfc-abs));
58
+
59
+ /* Effective values (can be overridden when hidden/active) */
60
+ --cfc-scale-effective: var(--cfc-scale);
61
+ --cfc-x-effective: var(--cfc-x);
62
+ --cfc-z-effective: var(--cfc-z);
63
+
64
+ pointer-events: auto;
65
+
66
+ position: absolute;
67
+ inset: 50% 0 0 50%;
68
+ width: var(--cfc-card-w);
69
+ aspect-ratio: var(--cfc-card-ar);
70
+ overflow: hidden;
71
+
72
+ z-index: var(--cfc-zIndex);
73
+
74
+ transform-style: preserve-3d;
75
+ transform: translate(-50%, -50%) translate3d(var(--cfc-x-effective), 0px, var(--cfc-z-effective))
76
+ scale(var(--cfc-scale-effective));
77
+
78
+ transition: transform var(--cfc-transition-ms) var(--cfc-easing);
79
+ }
80
+
81
+ .cfc__card[aria-hidden="false"] {
82
+ will-change: transform;
83
+ }
84
+
85
+ .cfc__card[aria-hidden="true"] {
86
+ /* CSS-only parking strategy */
87
+ --cfc-x-effective: var(--cfc-park-x);
88
+ --cfc-z-effective: var(--cfc-park-z);
89
+ --cfc-scale-effective: var(--cfc-park-scale);
90
+
91
+ pointer-events: none;
92
+ }
93
+
94
+ .cfc__card[data-active="true"] {
95
+ --cfc-scale-effective: 1;
96
+ }
97
+
98
+ .cfc__card ::slotted(img),
99
+ .cfc__card img {
100
+ width: 100%;
101
+ height: 100%;
102
+ display: block;
103
+ object-fit: cover;
104
+ object-position: center;
105
+ }
106
+
107
+ .cfc__arrow {
108
+ position: absolute;
109
+ top: calc(50% - var(--cfc-arrow-size));
110
+ width: var(--cfc-arrow-size);
111
+ height: var(--cfc-arrow-size);
112
+ border: none;
113
+ background: Crimson;
114
+ cursor: pointer;
115
+ z-index: 50;
116
+ transition: background 0.25s ease;
117
+ }
118
+
119
+ .cfc__arrow:hover {
120
+ background: LightGray;
121
+ }
122
+
123
+ .cfc__arrow--left {
124
+ left: 20px;
125
+ }
126
+
127
+ .cfc__arrow--right {
128
+ right: 20px;
129
+ }
130
+
131
+ .cfc__dots {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ gap: 10px;
136
+ height: 50px;
137
+ }
138
+
139
+ .cfc__dot {
140
+ width: 10px;
141
+ height: 10px;
142
+ border-radius: 50%;
143
+ background: LightGray;
144
+ cursor: default;
145
+ transition:
146
+ transform 0.25s ease,
147
+ background 0.25s ease;
148
+ }
149
+
150
+ .cfc__dot[data-active="true"] {
151
+ background: Crimson;
152
+ transform: scale(1.2);
153
+ }
154
+
155
+ /* SR-only */
156
+ .cfc__sr {
157
+ position: absolute;
158
+ width: 1px;
159
+ height: 1px;
160
+ padding: 0;
161
+ margin: -1px;
162
+ overflow: hidden;
163
+ clip: rect(0, 0, 0, 0);
164
+ white-space: nowrap;
165
+ border: 0;
166
+ }
167
+ `, D = v;
168
+ function f(a) {
169
+ return "adoptedStyleSheets" in a;
170
+ }
171
+ function b(a) {
172
+ try {
173
+ const t = new CSSStyleSheet();
174
+ return t.replaceSync(a), t;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ const S = b(v), I = v;
180
+ function p(a) {
181
+ return a.split(",").map((t) => t.trim()).filter(Boolean);
182
+ }
183
+ function m(a, t) {
184
+ const e = a.trim();
185
+ if (!e) return t;
186
+ if (e.endsWith("ms")) {
187
+ const n = Number(e.slice(0, -2).trim());
188
+ return Number.isFinite(n) ? n : t;
189
+ }
190
+ if (e.endsWith("s")) {
191
+ const n = Number(e.slice(0, -1).trim());
192
+ return Number.isFinite(n) ? n * 1e3 : t;
193
+ }
194
+ const i = Number(e);
195
+ return Number.isFinite(i) ? i : t;
196
+ }
197
+ function _(a, t) {
198
+ const e = p(a.transitionProperty), i = p(a.transitionDuration), n = p(a.transitionDelay);
199
+ if (!e.length) return 0;
200
+ const r = ((A) => {
201
+ const x = e.indexOf(A);
202
+ if (x >= 0) return x;
203
+ const g = e.indexOf("all");
204
+ return g >= 0 ? g : -1;
205
+ })(t);
206
+ if (r < 0) return 0;
207
+ const c = i[Math.min(r, i.length - 1)] ?? "0s", o = n[Math.min(r, n.length - 1)] ?? "0s", w = m(c, 0), E = m(o, 0);
208
+ return Math.max(0, w + E);
209
+ }
210
+ function k() {
211
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? !1;
212
+ }
213
+ function l(a, t, e) {
214
+ if (!a.hasAttribute(t)) return e;
215
+ const i = a.getAttribute(t);
216
+ return i == null || i === "" ? !0 : i !== "false";
217
+ }
218
+ function C(a, t, e) {
219
+ const i = a.getAttribute(t);
220
+ if (i == null) return e;
221
+ const n = Number(i.trim());
222
+ return Number.isFinite(n) ? Math.trunc(n) : e;
223
+ }
224
+ function y(a, t) {
225
+ const e = a.getAttribute(t);
226
+ if (e == null) return null;
227
+ const i = e.trim();
228
+ return i || null;
229
+ }
230
+ function d(a, t) {
231
+ return t <= 0 ? 0 : (a % t + t) % t;
232
+ }
233
+ function T(a, t, e) {
234
+ const i = t - a, n = e / 2;
235
+ return i > n ? i - e : i < -n ? i + e : i;
236
+ }
237
+ const h = 1;
238
+ let L = 0;
239
+ class u extends HTMLElement {
240
+ static defaultStylesheet = S;
241
+ static observedAttributes = [
242
+ "start-index",
243
+ "index",
244
+ "show-dots",
245
+ "show-arrows",
246
+ "announce-changes"
247
+ ];
248
+ shadow = this.attachShadow({ mode: "open" });
249
+ instanceId = `cfc-${++L}`;
250
+ rootEl;
251
+ trackEl;
252
+ dotsEl;
253
+ prevBtn;
254
+ nextBtn;
255
+ liveRegion;
256
+ styleEl = null;
257
+ cards = [];
258
+ dots = [];
259
+ currentIndex = 0;
260
+ isAnimating = !1;
261
+ hasAppliedInitialLayout = !1;
262
+ lastLayoutIndex = null;
263
+ lastVisibleSet = /* @__PURE__ */ new Set();
264
+ pendingAnimToken = 0;
265
+ animFallbackTimerId = null;
266
+ cleanup = [];
267
+ reflectGuard = !1;
268
+ connectedCallback() {
269
+ this.render(), this.readAttributes({ isInit: !0 }), this.refresh();
270
+ }
271
+ disconnectedCallback() {
272
+ this.destroy();
273
+ }
274
+ attributeChangedCallback(t, e, i) {
275
+ if (!this.isConnected || this.reflectGuard) return;
276
+ this.readAttributes({ isInit: !1 });
277
+ const n = y(this, "index");
278
+ if (n != null) {
279
+ const s = d(Number(n), this.cards.length);
280
+ if (Number.isFinite(s) && s !== this.currentIndex) {
281
+ this.goTo(s);
282
+ return;
283
+ }
284
+ }
285
+ this.applyLayoutAndA11y({ announce: !0, emitChange: !1 });
286
+ }
287
+ next() {
288
+ this.goTo(this.currentIndex + 1);
289
+ }
290
+ prev() {
291
+ this.goTo(this.currentIndex - 1);
292
+ }
293
+ goTo(t) {
294
+ if (this.isAnimating) return;
295
+ const e = d(t, this.cards.length);
296
+ e !== this.currentIndex && (this.isAnimating = !0, this.currentIndex = e, this.reflectIndexAttr(), this.applyLayoutAndA11y({ announce: !0, emitChange: !0 }), this.lockUntilTransitionEnd());
297
+ }
298
+ refresh() {
299
+ this.rebuildCardsFromLightDom();
300
+ const t = C(this, "start-index", 0), e = y(this, "index"), i = e != null ? Number(e) : t, n = Number.isFinite(i) ? i : 0;
301
+ this.currentIndex = d(n, this.cards.length), this.reflectIndexAttr(), this.buildDots(), this.hasAppliedInitialLayout = !1, this.lastLayoutIndex = null, this.lastVisibleSet = /* @__PURE__ */ new Set(), this.applyLayoutAndA11y({ announce: !0, emitChange: !1 }), this.dispatchReady();
302
+ }
303
+ destroy() {
304
+ this.animFallbackTimerId !== null && (window.clearTimeout(this.animFallbackTimerId), this.animFallbackTimerId = null), this.cleanup.forEach((t) => {
305
+ t();
306
+ }), this.cleanup = [], this.isAnimating = !1;
307
+ }
308
+ adoptStylesheet(t) {
309
+ this.applyStyles(t);
310
+ }
311
+ adoptStyles(t) {
312
+ this.applyStyles(t);
313
+ }
314
+ readAttributes(t) {
315
+ this.rootEl || this.render();
316
+ const e = l(this, "show-arrows", !1), i = l(this, "show-dots", !1);
317
+ this.prevBtn.style.display = e ? "" : "none", this.nextBtn.style.display = e ? "" : "none", this.dotsEl.style.display = i ? "" : "none";
318
+ const n = this.getAttribute("aria-label");
319
+ n && this.rootEl.setAttribute("aria-label", n), t.isInit && this.setAttribute("aria-roledescription", "carousel");
320
+ }
321
+ render() {
322
+ this.applyStyles(null), this.rootEl = document.createElement("div"), this.rootEl.className = "cfc", this.trackEl = document.createElement("div"), this.trackEl.className = "cfc__track", this.prevBtn = document.createElement("button"), this.prevBtn.type = "button", this.prevBtn.className = "cfc__arrow cfc__arrow--left", this.prevBtn.setAttribute("aria-label", "Previous"), this.prevBtn.textContent = "‹", this.nextBtn = document.createElement("button"), this.nextBtn.type = "button", this.nextBtn.className = "cfc__arrow cfc__arrow--right", this.nextBtn.setAttribute("aria-label", "Next"), this.nextBtn.textContent = "›", this.dotsEl = document.createElement("div"), this.dotsEl.className = "cfc__dots", this.liveRegion = document.createElement("div"), this.liveRegion.className = "cfc__sr", this.liveRegion.setAttribute("aria-live", "polite"), this.liveRegion.setAttribute("aria-atomic", "true"), this.rootEl.append(this.trackEl, this.prevBtn, this.nextBtn, this.dotsEl, this.liveRegion), this.shadow.innerHTML = "", this.styleEl && this.shadow.append(this.styleEl), this.shadow.append(this.rootEl), this.bindEvents();
323
+ }
324
+ bindEvents() {
325
+ this.cleanup.forEach((n) => {
326
+ n();
327
+ }), this.cleanup = [];
328
+ const t = () => this.prev(), e = () => this.next();
329
+ this.prevBtn.addEventListener("click", t), this.nextBtn.addEventListener("click", e), this.cleanup.push(() => this.prevBtn.removeEventListener("click", t)), this.cleanup.push(() => this.nextBtn.removeEventListener("click", e));
330
+ const i = (n) => {
331
+ if (!this.isAnimating || n.propertyName !== "transform" || !(n.target instanceof HTMLElement)) return;
332
+ const s = this.cards[this.currentIndex];
333
+ s && n.target === s && this.unlockAnimation();
334
+ };
335
+ this.rootEl.addEventListener("transitionend", i), this.cleanup.push(() => this.rootEl.removeEventListener("transitionend", i));
336
+ }
337
+ rebuildCardsFromLightDom() {
338
+ const t = Array.from(this.trackEl.children).filter(
339
+ (r) => r instanceof HTMLElement && r.classList.contains("cfc__card")
340
+ ), e = [];
341
+ t.forEach((r) => {
342
+ const c = r.firstElementChild;
343
+ c instanceof HTMLElement && e.push(c);
344
+ });
345
+ const i = Array.from(this.children).filter(
346
+ (r) => r instanceof HTMLElement
347
+ ), n = [...e, ...i], s = [];
348
+ for (let r = 0; r < n.length; r++) {
349
+ const c = n[r], o = t[r] ?? document.createElement("div");
350
+ t[r] || (o.className = "cfc__card", this.trackEl.append(o)), o.firstElementChild !== c && o.replaceChildren(c), s.push(o);
351
+ }
352
+ for (let r = n.length; r < t.length; r++)
353
+ t[r]?.remove();
354
+ this.cards = s, this.hasAppliedInitialLayout = !1, this.lastLayoutIndex = null, this.lastVisibleSet = /* @__PURE__ */ new Set();
355
+ }
356
+ getVisibleSet() {
357
+ const t = this.cards.length, e = /* @__PURE__ */ new Set();
358
+ if (t <= 0) return e;
359
+ const i = Math.floor(t / 2);
360
+ if (h >= i) {
361
+ for (let n = 0; n < t; n++) e.add(n);
362
+ return e;
363
+ }
364
+ for (let n = -h; n <= h; n++)
365
+ e.add(d(this.currentIndex + n, t));
366
+ return e;
367
+ }
368
+ buildDots() {
369
+ const t = Array.from(this.dotsEl.children).filter(
370
+ (s) => s instanceof HTMLElement && s.classList.contains("cfc__dot")
371
+ );
372
+ if (!l(this, "show-dots", !1)) {
373
+ t.forEach((s) => {
374
+ s.remove();
375
+ }), this.dots = [];
376
+ return;
377
+ }
378
+ const i = [], n = this.cards.length;
379
+ for (let s = 0; s < n; s++) {
380
+ const r = t[s] ?? document.createElement("span");
381
+ t[s] || this.dotsEl.append(r), r.className = "cfc__dot", r.setAttribute("aria-hidden", "true"), i.push(r);
382
+ }
383
+ for (let s = n; s < t.length; s++)
384
+ t[s]?.remove();
385
+ this.dots = i, this.updateDotsVisualState();
386
+ }
387
+ computeCardState(t) {
388
+ const e = this.cards.length, i = T(this.currentIndex, t, e), n = Math.abs(i);
389
+ return {
390
+ index: t,
391
+ delta: i,
392
+ abs: n,
393
+ isVisible: n <= h,
394
+ isActive: t === this.currentIndex
395
+ };
396
+ }
397
+ applyCardState(t, e) {
398
+ t.setAttribute("aria-hidden", e.isVisible ? "false" : "true"), t.dataset.active = e.isActive ? "true" : "false", t.setAttribute("role", "group"), t.setAttribute("aria-roledescription", "slide"), t.setAttribute("aria-setsize", String(this.cards.length)), t.setAttribute("aria-posinset", String(e.index + 1));
399
+ const i = `${this.instanceId}-slide-${e.index}`;
400
+ t.id = i;
401
+ const n = String(e.delta), s = String(e.abs), r = t.dataset.cfcDelta, c = t.dataset.cfcAbs;
402
+ r !== n && (t.style.setProperty("--cfc-delta", n), t.dataset.cfcDelta = n), c !== s && (t.style.setProperty("--cfc-abs", s), t.dataset.cfcAbs = s);
403
+ }
404
+ applyLayoutAndA11y(t) {
405
+ if (this.cards.length) {
406
+ if (this.hasAppliedInitialLayout) {
407
+ const e = this.getVisibleSet(), i = /* @__PURE__ */ new Set();
408
+ this.lastVisibleSet.forEach((n) => {
409
+ i.add(n);
410
+ }), e.forEach((n) => {
411
+ i.add(n);
412
+ }), this.lastLayoutIndex != null && i.add(this.lastLayoutIndex), i.add(this.currentIndex), i.forEach((n) => {
413
+ const s = this.cards[n];
414
+ if (!s) return;
415
+ const r = this.computeCardState(n);
416
+ this.applyCardState(s, r);
417
+ }), this.lastLayoutIndex = this.currentIndex, this.lastVisibleSet = e;
418
+ } else {
419
+ for (let e = 0; e < this.cards.length; e++) {
420
+ const i = this.cards[e];
421
+ if (!i) continue;
422
+ const n = this.computeCardState(e);
423
+ this.applyCardState(i, n);
424
+ }
425
+ this.hasAppliedInitialLayout = !0, this.lastLayoutIndex = this.currentIndex, this.lastVisibleSet = this.getVisibleSet();
426
+ }
427
+ this.updateDotsVisualState(), t.announce && l(this, "announce-changes", !0) && this.announce(`Slide ${this.currentIndex + 1} of ${this.cards.length}`), t.emitChange && this.emitChange();
428
+ }
429
+ }
430
+ updateDotsVisualState() {
431
+ if (this.dots.length)
432
+ for (let t = 0; t < this.dots.length; t++) {
433
+ const e = this.dots[t], i = t === this.currentIndex;
434
+ e.dataset.active = i ? "true" : "false";
435
+ }
436
+ }
437
+ lockUntilTransitionEnd() {
438
+ this.pendingAnimToken++;
439
+ const t = this.pendingAnimToken;
440
+ if (k()) {
441
+ this.unlockAnimation();
442
+ return;
443
+ }
444
+ const e = this.cards[this.currentIndex];
445
+ if (!e) {
446
+ this.unlockAnimation();
447
+ return;
448
+ }
449
+ const i = getComputedStyle(e), n = _(i, "transform");
450
+ if (n <= 0) {
451
+ this.unlockAnimation();
452
+ return;
453
+ }
454
+ const s = m(i.getPropertyValue("--cfc-transition-ms").trim(), 400), c = Math.max(n, s) + 60;
455
+ this.animFallbackTimerId !== null && window.clearTimeout(this.animFallbackTimerId), this.animFallbackTimerId = window.setTimeout(() => {
456
+ this.pendingAnimToken === t && this.unlockAnimation();
457
+ }, c);
458
+ }
459
+ unlockAnimation() {
460
+ this.isAnimating = !1, this.animFallbackTimerId !== null && (window.clearTimeout(this.animFallbackTimerId), this.animFallbackTimerId = null);
461
+ }
462
+ emitChange() {
463
+ this.dispatchEvent(
464
+ new CustomEvent("coverflow-carousel:change", {
465
+ detail: { index: this.currentIndex, length: this.cards.length }
466
+ })
467
+ );
468
+ }
469
+ dispatchReady() {
470
+ this.dispatchEvent(
471
+ new CustomEvent("coverflow-carousel:ready", {
472
+ detail: { index: this.currentIndex, length: this.cards.length }
473
+ })
474
+ );
475
+ }
476
+ announce(t) {
477
+ this.liveRegion.textContent = "", queueMicrotask(() => {
478
+ this.liveRegion.textContent = t;
479
+ });
480
+ }
481
+ reflectIndexAttr() {
482
+ this.reflectGuard = !0;
483
+ try {
484
+ this.setAttribute("index", String(this.currentIndex));
485
+ } finally {
486
+ this.reflectGuard = !1;
487
+ }
488
+ }
489
+ applyStyles(t) {
490
+ if (typeof t == "string") {
491
+ if (f(this.shadow)) {
492
+ const e = b(t);
493
+ if (e) {
494
+ this.shadow.adoptedStyleSheets = [e], this.styleEl = null;
495
+ return;
496
+ }
497
+ }
498
+ this.styleEl || (this.styleEl = document.createElement("style")), this.styleEl.textContent = t;
499
+ return;
500
+ }
501
+ if (t && f(this.shadow)) {
502
+ this.shadow.adoptedStyleSheets = [t], this.styleEl = null;
503
+ return;
504
+ }
505
+ if (u.defaultStylesheet && f(this.shadow)) {
506
+ this.shadow.adoptedStyleSheets = [u.defaultStylesheet], this.styleEl = null;
507
+ return;
508
+ }
509
+ this.styleEl || (this.styleEl = document.createElement("style")), this.styleEl.textContent = I;
510
+ }
511
+ }
512
+ function N(a = {}) {
513
+ const { selector: t = "coverflow-carousel", onReady: e, onChange: i, stylesheet: n } = a, s = Array.from(document.querySelectorAll(t));
514
+ return (e || i) && s.forEach((r) => {
515
+ e && r.addEventListener("coverflow-carousel:ready", (c) => {
516
+ e(r, c.detail);
517
+ }), i && r.addEventListener("coverflow-carousel:change", (c) => {
518
+ i(r, c.detail);
519
+ });
520
+ }), n && s.forEach((r) => {
521
+ const c = r;
522
+ typeof n == "string" ? c.adoptStyles(n) : c.adoptStylesheet(n);
523
+ }), s;
524
+ }
525
+ customElements.get("coverflow-carousel") || customElements.define("coverflow-carousel", u);
526
+ export {
527
+ D as coverflowCarouselCssText,
528
+ N as initCoverflowCarousels
529
+ };
@@ -0,0 +1,167 @@
1
+ (function(l,d){typeof exports=="object"&&typeof module<"u"?d(exports):typeof define=="function"&&define.amd?define(["exports"],d):(l=typeof globalThis<"u"?globalThis:l||self,d(l.CoverflowCarousel={}))})(this,(function(l){"use strict";const d=`:host {
2
+ display: block;
3
+ width: 100%;
4
+ height: 100%;
5
+ container-type: inline-size;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .cfc {
10
+ /* Layout */
11
+ --cfc-card-ar-num: 1.25;
12
+ --cfc-card-w: clamp(160px, 100vw - 120px, 380px);
13
+ --cfc-card-ar: 1 / var(--cfc-card-ar-num);
14
+
15
+ /* Motion / depth */
16
+ --cfc-step-x: min(22vw, 250px);
17
+ --cfc-step-z: calc(var(--cfc-card-w) * 0.5);
18
+
19
+ /* Visual falloff */
20
+ --cfc-scale-step: 0.1;
21
+
22
+ /* Transitions */
23
+ --cfc-transition-ms: 550ms;
24
+ --cfc-easing: cubic-bezier(0.785, 0.135, 0.15, 0.86);
25
+
26
+ /* Controls */
27
+ --cfc-arrow-size: 50px;
28
+
29
+ /* CSS-only "parking strategy" for hidden cards */
30
+ --cfc-park-x: 0px;
31
+ --cfc-park-z: calc(var(--cfc-step-z) * -6);
32
+ --cfc-park-scale: 0.1;
33
+
34
+ margin: auto;
35
+ width: min(900px, 100%);
36
+ height: 100%;
37
+ position: relative;
38
+ perspective: 1000px;
39
+ }
40
+
41
+ .cfc__track {
42
+ width: 100%;
43
+ height: calc(var(--cfc-card-w) * var(--cfc-card-ar-num));
44
+ position: relative;
45
+ transform-style: preserve-3d;
46
+ }
47
+
48
+ .cfc__card {
49
+ /* Inputs from JS */
50
+ --cfc-delta: 0;
51
+ --cfc-abs: 0;
52
+
53
+ /* Derived values */
54
+ --cfc-scale: clamp(0.1, calc(1 - (var(--cfc-abs) * var(--cfc-scale-step))), 1);
55
+ --cfc-x: calc(var(--cfc-step-x) * var(--cfc-delta));
56
+ --cfc-z: calc(var(--cfc-step-z) * -1 * var(--cfc-abs));
57
+ --cfc-zIndex: calc(100 - var(--cfc-abs));
58
+
59
+ /* Effective values (can be overridden when hidden/active) */
60
+ --cfc-scale-effective: var(--cfc-scale);
61
+ --cfc-x-effective: var(--cfc-x);
62
+ --cfc-z-effective: var(--cfc-z);
63
+
64
+ pointer-events: auto;
65
+
66
+ position: absolute;
67
+ inset: 50% 0 0 50%;
68
+ width: var(--cfc-card-w);
69
+ aspect-ratio: var(--cfc-card-ar);
70
+ overflow: hidden;
71
+
72
+ z-index: var(--cfc-zIndex);
73
+
74
+ transform-style: preserve-3d;
75
+ transform: translate(-50%, -50%) translate3d(var(--cfc-x-effective), 0px, var(--cfc-z-effective))
76
+ scale(var(--cfc-scale-effective));
77
+
78
+ transition: transform var(--cfc-transition-ms) var(--cfc-easing);
79
+ }
80
+
81
+ .cfc__card[aria-hidden="false"] {
82
+ will-change: transform;
83
+ }
84
+
85
+ .cfc__card[aria-hidden="true"] {
86
+ /* CSS-only parking strategy */
87
+ --cfc-x-effective: var(--cfc-park-x);
88
+ --cfc-z-effective: var(--cfc-park-z);
89
+ --cfc-scale-effective: var(--cfc-park-scale);
90
+
91
+ pointer-events: none;
92
+ }
93
+
94
+ .cfc__card[data-active="true"] {
95
+ --cfc-scale-effective: 1;
96
+ }
97
+
98
+ .cfc__card ::slotted(img),
99
+ .cfc__card img {
100
+ width: 100%;
101
+ height: 100%;
102
+ display: block;
103
+ object-fit: cover;
104
+ object-position: center;
105
+ }
106
+
107
+ .cfc__arrow {
108
+ position: absolute;
109
+ top: calc(50% - var(--cfc-arrow-size));
110
+ width: var(--cfc-arrow-size);
111
+ height: var(--cfc-arrow-size);
112
+ border: none;
113
+ background: Crimson;
114
+ cursor: pointer;
115
+ z-index: 50;
116
+ transition: background 0.25s ease;
117
+ }
118
+
119
+ .cfc__arrow:hover {
120
+ background: LightGray;
121
+ }
122
+
123
+ .cfc__arrow--left {
124
+ left: 20px;
125
+ }
126
+
127
+ .cfc__arrow--right {
128
+ right: 20px;
129
+ }
130
+
131
+ .cfc__dots {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ gap: 10px;
136
+ height: 50px;
137
+ }
138
+
139
+ .cfc__dot {
140
+ width: 10px;
141
+ height: 10px;
142
+ border-radius: 50%;
143
+ background: LightGray;
144
+ cursor: default;
145
+ transition:
146
+ transform 0.25s ease,
147
+ background 0.25s ease;
148
+ }
149
+
150
+ .cfc__dot[data-active="true"] {
151
+ background: Crimson;
152
+ transform: scale(1.2);
153
+ }
154
+
155
+ /* SR-only */
156
+ .cfc__sr {
157
+ position: absolute;
158
+ width: 1px;
159
+ height: 1px;
160
+ padding: 0;
161
+ margin: -1px;
162
+ overflow: hidden;
163
+ clip: rect(0, 0, 0, 0);
164
+ white-space: nowrap;
165
+ border: 0;
166
+ }
167
+ `,E=d;function m(a){return"adoptedStyleSheets"in a}function g(a){try{const t=new CSSStyleSheet;return t.replaceSync(a),t}catch{return null}}const A=g(d),S=d;function v(a){return a.split(",").map(t=>t.trim()).filter(Boolean)}function x(a,t){const e=a.trim();if(!e)return t;if(e.endsWith("ms")){const n=Number(e.slice(0,-2).trim());return Number.isFinite(n)?n:t}if(e.endsWith("s")){const n=Number(e.slice(0,-1).trim());return Number.isFinite(n)?n*1e3:t}const i=Number(e);return Number.isFinite(i)?i:t}function I(a,t){const e=v(a.transitionProperty),i=v(a.transitionDuration),n=v(a.transitionDelay);if(!e.length)return 0;const r=(F=>{const b=e.indexOf(F);if(b>=0)return b;const w=e.indexOf("all");return w>=0?w:-1})(t);if(r<0)return 0;const c=i[Math.min(r,i.length-1)]??"0s",o=n[Math.min(r,n.length-1)]??"0s",D=x(c,0),N=x(o,0);return Math.max(0,D+N)}function _(){return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches??!1}function h(a,t,e){if(!a.hasAttribute(t))return e;const i=a.getAttribute(t);return i==null||i===""?!0:i!=="false"}function k(a,t,e){const i=a.getAttribute(t);if(i==null)return e;const n=Number(i.trim());return Number.isFinite(n)?Math.trunc(n):e}function y(a,t){const e=a.getAttribute(t);if(e==null)return null;const i=e.trim();return i||null}function u(a,t){return t<=0?0:(a%t+t)%t}function C(a,t,e){const i=t-a,n=e/2;return i>n?i-e:i<-n?i+e:i}const f=1;let T=0;class p extends HTMLElement{static defaultStylesheet=A;static observedAttributes=["start-index","index","show-dots","show-arrows","announce-changes"];shadow=this.attachShadow({mode:"open"});instanceId=`cfc-${++T}`;rootEl;trackEl;dotsEl;prevBtn;nextBtn;liveRegion;styleEl=null;cards=[];dots=[];currentIndex=0;isAnimating=!1;hasAppliedInitialLayout=!1;lastLayoutIndex=null;lastVisibleSet=new Set;pendingAnimToken=0;animFallbackTimerId=null;cleanup=[];reflectGuard=!1;connectedCallback(){this.render(),this.readAttributes({isInit:!0}),this.refresh()}disconnectedCallback(){this.destroy()}attributeChangedCallback(t,e,i){if(!this.isConnected||this.reflectGuard)return;this.readAttributes({isInit:!1});const n=y(this,"index");if(n!=null){const s=u(Number(n),this.cards.length);if(Number.isFinite(s)&&s!==this.currentIndex){this.goTo(s);return}}this.applyLayoutAndA11y({announce:!0,emitChange:!1})}next(){this.goTo(this.currentIndex+1)}prev(){this.goTo(this.currentIndex-1)}goTo(t){if(this.isAnimating)return;const e=u(t,this.cards.length);e!==this.currentIndex&&(this.isAnimating=!0,this.currentIndex=e,this.reflectIndexAttr(),this.applyLayoutAndA11y({announce:!0,emitChange:!0}),this.lockUntilTransitionEnd())}refresh(){this.rebuildCardsFromLightDom();const t=k(this,"start-index",0),e=y(this,"index"),i=e!=null?Number(e):t,n=Number.isFinite(i)?i:0;this.currentIndex=u(n,this.cards.length),this.reflectIndexAttr(),this.buildDots(),this.hasAppliedInitialLayout=!1,this.lastLayoutIndex=null,this.lastVisibleSet=new Set,this.applyLayoutAndA11y({announce:!0,emitChange:!1}),this.dispatchReady()}destroy(){this.animFallbackTimerId!==null&&(window.clearTimeout(this.animFallbackTimerId),this.animFallbackTimerId=null),this.cleanup.forEach(t=>{t()}),this.cleanup=[],this.isAnimating=!1}adoptStylesheet(t){this.applyStyles(t)}adoptStyles(t){this.applyStyles(t)}readAttributes(t){this.rootEl||this.render();const e=h(this,"show-arrows",!1),i=h(this,"show-dots",!1);this.prevBtn.style.display=e?"":"none",this.nextBtn.style.display=e?"":"none",this.dotsEl.style.display=i?"":"none";const n=this.getAttribute("aria-label");n&&this.rootEl.setAttribute("aria-label",n),t.isInit&&this.setAttribute("aria-roledescription","carousel")}render(){this.applyStyles(null),this.rootEl=document.createElement("div"),this.rootEl.className="cfc",this.trackEl=document.createElement("div"),this.trackEl.className="cfc__track",this.prevBtn=document.createElement("button"),this.prevBtn.type="button",this.prevBtn.className="cfc__arrow cfc__arrow--left",this.prevBtn.setAttribute("aria-label","Previous"),this.prevBtn.textContent="‹",this.nextBtn=document.createElement("button"),this.nextBtn.type="button",this.nextBtn.className="cfc__arrow cfc__arrow--right",this.nextBtn.setAttribute("aria-label","Next"),this.nextBtn.textContent="›",this.dotsEl=document.createElement("div"),this.dotsEl.className="cfc__dots",this.liveRegion=document.createElement("div"),this.liveRegion.className="cfc__sr",this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("aria-atomic","true"),this.rootEl.append(this.trackEl,this.prevBtn,this.nextBtn,this.dotsEl,this.liveRegion),this.shadow.innerHTML="",this.styleEl&&this.shadow.append(this.styleEl),this.shadow.append(this.rootEl),this.bindEvents()}bindEvents(){this.cleanup.forEach(n=>{n()}),this.cleanup=[];const t=()=>this.prev(),e=()=>this.next();this.prevBtn.addEventListener("click",t),this.nextBtn.addEventListener("click",e),this.cleanup.push(()=>this.prevBtn.removeEventListener("click",t)),this.cleanup.push(()=>this.nextBtn.removeEventListener("click",e));const i=n=>{if(!this.isAnimating||n.propertyName!=="transform"||!(n.target instanceof HTMLElement))return;const s=this.cards[this.currentIndex];s&&n.target===s&&this.unlockAnimation()};this.rootEl.addEventListener("transitionend",i),this.cleanup.push(()=>this.rootEl.removeEventListener("transitionend",i))}rebuildCardsFromLightDom(){const t=Array.from(this.trackEl.children).filter(r=>r instanceof HTMLElement&&r.classList.contains("cfc__card")),e=[];t.forEach(r=>{const c=r.firstElementChild;c instanceof HTMLElement&&e.push(c)});const i=Array.from(this.children).filter(r=>r instanceof HTMLElement),n=[...e,...i],s=[];for(let r=0;r<n.length;r++){const c=n[r],o=t[r]??document.createElement("div");t[r]||(o.className="cfc__card",this.trackEl.append(o)),o.firstElementChild!==c&&o.replaceChildren(c),s.push(o)}for(let r=n.length;r<t.length;r++)t[r]?.remove();this.cards=s,this.hasAppliedInitialLayout=!1,this.lastLayoutIndex=null,this.lastVisibleSet=new Set}getVisibleSet(){const t=this.cards.length,e=new Set;if(t<=0)return e;const i=Math.floor(t/2);if(f>=i){for(let n=0;n<t;n++)e.add(n);return e}for(let n=-f;n<=f;n++)e.add(u(this.currentIndex+n,t));return e}buildDots(){const t=Array.from(this.dotsEl.children).filter(s=>s instanceof HTMLElement&&s.classList.contains("cfc__dot"));if(!h(this,"show-dots",!1)){t.forEach(s=>{s.remove()}),this.dots=[];return}const i=[],n=this.cards.length;for(let s=0;s<n;s++){const r=t[s]??document.createElement("span");t[s]||this.dotsEl.append(r),r.className="cfc__dot",r.setAttribute("aria-hidden","true"),i.push(r)}for(let s=n;s<t.length;s++)t[s]?.remove();this.dots=i,this.updateDotsVisualState()}computeCardState(t){const e=this.cards.length,i=C(this.currentIndex,t,e),n=Math.abs(i);return{index:t,delta:i,abs:n,isVisible:n<=f,isActive:t===this.currentIndex}}applyCardState(t,e){t.setAttribute("aria-hidden",e.isVisible?"false":"true"),t.dataset.active=e.isActive?"true":"false",t.setAttribute("role","group"),t.setAttribute("aria-roledescription","slide"),t.setAttribute("aria-setsize",String(this.cards.length)),t.setAttribute("aria-posinset",String(e.index+1));const i=`${this.instanceId}-slide-${e.index}`;t.id=i;const n=String(e.delta),s=String(e.abs),r=t.dataset.cfcDelta,c=t.dataset.cfcAbs;r!==n&&(t.style.setProperty("--cfc-delta",n),t.dataset.cfcDelta=n),c!==s&&(t.style.setProperty("--cfc-abs",s),t.dataset.cfcAbs=s)}applyLayoutAndA11y(t){if(this.cards.length){if(this.hasAppliedInitialLayout){const e=this.getVisibleSet(),i=new Set;this.lastVisibleSet.forEach(n=>{i.add(n)}),e.forEach(n=>{i.add(n)}),this.lastLayoutIndex!=null&&i.add(this.lastLayoutIndex),i.add(this.currentIndex),i.forEach(n=>{const s=this.cards[n];if(!s)return;const r=this.computeCardState(n);this.applyCardState(s,r)}),this.lastLayoutIndex=this.currentIndex,this.lastVisibleSet=e}else{for(let e=0;e<this.cards.length;e++){const i=this.cards[e];if(!i)continue;const n=this.computeCardState(e);this.applyCardState(i,n)}this.hasAppliedInitialLayout=!0,this.lastLayoutIndex=this.currentIndex,this.lastVisibleSet=this.getVisibleSet()}this.updateDotsVisualState(),t.announce&&h(this,"announce-changes",!0)&&this.announce(`Slide ${this.currentIndex+1} of ${this.cards.length}`),t.emitChange&&this.emitChange()}}updateDotsVisualState(){if(this.dots.length)for(let t=0;t<this.dots.length;t++){const e=this.dots[t],i=t===this.currentIndex;e.dataset.active=i?"true":"false"}}lockUntilTransitionEnd(){this.pendingAnimToken++;const t=this.pendingAnimToken;if(_()){this.unlockAnimation();return}const e=this.cards[this.currentIndex];if(!e){this.unlockAnimation();return}const i=getComputedStyle(e),n=I(i,"transform");if(n<=0){this.unlockAnimation();return}const s=x(i.getPropertyValue("--cfc-transition-ms").trim(),400),c=Math.max(n,s)+60;this.animFallbackTimerId!==null&&window.clearTimeout(this.animFallbackTimerId),this.animFallbackTimerId=window.setTimeout(()=>{this.pendingAnimToken===t&&this.unlockAnimation()},c)}unlockAnimation(){this.isAnimating=!1,this.animFallbackTimerId!==null&&(window.clearTimeout(this.animFallbackTimerId),this.animFallbackTimerId=null)}emitChange(){this.dispatchEvent(new CustomEvent("coverflow-carousel:change",{detail:{index:this.currentIndex,length:this.cards.length}}))}dispatchReady(){this.dispatchEvent(new CustomEvent("coverflow-carousel:ready",{detail:{index:this.currentIndex,length:this.cards.length}}))}announce(t){this.liveRegion.textContent="",queueMicrotask(()=>{this.liveRegion.textContent=t})}reflectIndexAttr(){this.reflectGuard=!0;try{this.setAttribute("index",String(this.currentIndex))}finally{this.reflectGuard=!1}}applyStyles(t){if(typeof t=="string"){if(m(this.shadow)){const e=g(t);if(e){this.shadow.adoptedStyleSheets=[e],this.styleEl=null;return}}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=t;return}if(t&&m(this.shadow)){this.shadow.adoptedStyleSheets=[t],this.styleEl=null;return}if(p.defaultStylesheet&&m(this.shadow)){this.shadow.adoptedStyleSheets=[p.defaultStylesheet],this.styleEl=null;return}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=S}}function L(a={}){const{selector:t="coverflow-carousel",onReady:e,onChange:i,stylesheet:n}=a,s=Array.from(document.querySelectorAll(t));return(e||i)&&s.forEach(r=>{e&&r.addEventListener("coverflow-carousel:ready",c=>{e(r,c.detail)}),i&&r.addEventListener("coverflow-carousel:change",c=>{i(r,c.detail)})}),n&&s.forEach(r=>{const c=r;typeof n=="string"?c.adoptStyles(n):c.adoptStylesheet(n)}),s}customElements.get("coverflow-carousel")||customElements.define("coverflow-carousel",p),l.coverflowCarouselCssText=E,l.initCoverflowCarousels=L,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"})}));
package/dist/init.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type InitCoverflowCarouselsOptions = {
2
+ selector?: string;
3
+ onReady?: (el: HTMLElement, detail: {
4
+ index: number;
5
+ length: number;
6
+ }) => void;
7
+ onChange?: (el: HTMLElement, detail: {
8
+ index: number;
9
+ length: number;
10
+ }) => void;
11
+ stylesheet?: CSSStyleSheet | string | null;
12
+ };
13
+ export declare function initCoverflowCarousels(options?: InitCoverflowCarouselsOptions): HTMLElement[];
@@ -0,0 +1,6 @@
1
+ export declare const coverflowCarouselCssText: string;
2
+ export type StylesInput = CSSStyleSheet | string | null | undefined;
3
+ export declare function supportsAdoptedStyleSheets(root: ShadowRoot): boolean;
4
+ export declare function makeConstructableSheet(cssText: string): CSSStyleSheet | null;
5
+ export declare const DEFAULT_STYLESHEET: CSSStyleSheet | null;
6
+ export declare const DEFAULT_CSS_TEXT_RAW: string;
@@ -0,0 +1,3 @@
1
+ export declare function parseDurationToMs(raw: string, fallbackMs: number): number;
2
+ export declare function getTransitionMsForPropertyFromComputedStyle(cs: CSSStyleDeclaration, prop: string): number;
3
+ export declare function getTransitionMsForProperty(el: HTMLElement, prop: string): number;
@@ -0,0 +1,4 @@
1
+ export declare function prefersReducedMotion(): boolean;
2
+ export declare function hasBoolAttr(el: HTMLElement, name: string, defaultValue: boolean): boolean;
3
+ export declare function readIntAttr(el: HTMLElement, name: string, fallback: number): number;
4
+ export declare function readStringAttr(el: HTMLElement, name: string): string | null;
@@ -0,0 +1,2 @@
1
+ export declare function normalizeIndex(index: number, length: number): number;
2
+ export declare function circularDelta(from: number, to: number, n: number): number;
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "coverflow-carousel",
3
+ "version": "0.1.0-dev.0",
4
+ "description": "Tiny coverflow carousel Web Component. Registers <coverflow-carousel> and exposes API via attributes + events.",
5
+ "author": "ux-ui.pro",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ux-ui-pro/coverflow-carousel.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ux-ui-pro/coverflow-carousel/issues"
13
+ },
14
+ "homepage": "https://github.com/ux-ui-pro/coverflow-carousel",
15
+ "sideEffects": true,
16
+ "scripts": {
17
+ "clean": "rimraf dist",
18
+ "build": "vite build",
19
+ "dev": "vite",
20
+ "verify": "yarn lint && yarn typecheck",
21
+ "lint": "biome check src",
22
+ "lint:fix": "biome check --write src",
23
+ "format": "biome format --write src",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit"
25
+ },
26
+ "source": "src/index.ts",
27
+ "main": "dist/index.cjs.js",
28
+ "module": "dist/index.es.js",
29
+ "types": "dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.es.js",
34
+ "require": "./dist/index.cjs.js"
35
+ },
36
+ "./coverflow-carousel.css": "./dist/coverflow-carousel.css",
37
+ "./dist/*": "./dist/*"
38
+ },
39
+ "files": [
40
+ "dist/"
41
+ ],
42
+ "devDependencies": {
43
+ "@biomejs/biome": "2.3.10",
44
+ "@types/node": "25.0.3",
45
+ "lightningcss": "1.30.2",
46
+ "rimraf": "6.1.2",
47
+ "typescript": "5.9.3",
48
+ "vite": "7.3.0",
49
+ "vite-plugin-dts": "4.5.4"
50
+ },
51
+ "keywords": [
52
+ "typescript",
53
+ "frontend",
54
+ "library",
55
+ "carousel",
56
+ "coverflow",
57
+ "web component",
58
+ "web-component",
59
+ "custom elements",
60
+ "custom-elements",
61
+ "shadow dom",
62
+ "shadow-dom",
63
+ "ui",
64
+ "component",
65
+ "accessibility",
66
+ "a11y",
67
+ "coverflow-carousel"
68
+ ]
69
+ }