diff --git a/themes/origins/assets/component-slider.css b/themes/origins/assets/component-slider.css new file mode 100644 index 0000000..c5fbbbf --- /dev/null +++ b/themes/origins/assets/component-slider.css @@ -0,0 +1,52 @@ +yc-slider{ + display:grid; + position:relative; + margin:2em 10%; + height:calc(var(--slider-height) * 1px); +} +yc-slider .slider-area{ + display:flex; +} +yc-slider .slider-area .slider-box{ + touch-action:pan-y; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + overflow:hidden; + height:100%; + width:100%; +} +yc-slider .slider-area .slider-box .slider-inner{ + height:100%; + display:flex; + gap:calc(var(--slider-gap) * 1px); + transition:transform cubic-bezier(0.25, 1, 0.5, 1); + transition-duration:calc(var(--slider-speed) * 1ms); +} +yc-slider .slider-area .slider-box .slider-inner .slider-item{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + min-width:calc(100% / var(--slider-per-page) - var(--slider-gap) * 1px + var(--slider-gap) / var(--slider-per-page) * 1px); +} +yc-slider .slider-arrows{ + position:absolute; + display:flex; + align-items:center; + gap:var(--gap-lg); + position:absolute; + inset-block-end:1em; + translate:-50% -50%; + inset-inline-start:50%; +} +yc-slider .slider-arrows button{ + padding:8px 15px; + color:var(--color-primary); + background-color:var(--color-secondary); + border-radius:var(--radius-sm); + box-shadow:inset rgba(50, 50, 93, 0.1) 0px 2px 5px -1px, rgba(0, 0, 0, 0.2) 0px 1px 3px -1px; +} +yc-slider .slider-arrows button:disabled{ + opacity:0.5; + cursor:not-allowed; +} diff --git a/themes/origins/assets/component-slider.js b/themes/origins/assets/component-slider.js new file mode 100644 index 0000000..485fa5d --- /dev/null +++ b/themes/origins/assets/component-slider.js @@ -0,0 +1,153 @@ +class Slider extends HTMLElement { + static observedAttributes = ["autoplay", "interval", "pause-on-hover"]; + + constructor() { + super(); + + this.slider = this.querySelector("[data-slider]"); + this.sliderBox = this.querySelector("[data-slider-box]"); + this.leftArrow = this.querySelector("[data-arrow='left']"); + this.rightArrow = this.querySelector("[data-arrow='right']"); + + this.index = 0; + this.startX = 0; + this.currentX = 0; + this.interval = null; + this.isSwiping = false; + this.isBoundaryAllowed = true; + + this.TOTAL = this.slider.children.length; + this.GAP = this._getStyle("slider-gap", 0); + this.SPEED = this._getStyle("slider-speed", 600); + this.PER_PAGE = this._getStyle("slider-per-page", 1); + this.INTERVAL_DURATION = + parseInt(this.getAttribute("interval"), 10) || 3000; + + this.SWIPE_EVENTS = { + onSwipe: ["touchmove", "mousemove"], + startSwipe: ["touchstart", "mousedown"], + endSwipe: ["touchend", "mouseup", "mouseleave"], + }; + } + + connectedCallback() { + this._render(); + } + + _getStyle(variable, default_value) { + return ( + parseInt(getComputedStyle(this).getPropertyValue(`--${variable}`), 10) || + default_value + ); + } + + _render() { + this.hasAttribute("auto-play") && this.autoPlay(); + + this.leftArrow.addEventListener("click", () => this.setIndex(-1)); + this.rightArrow.addEventListener("click", () => this.setIndex(1)); + + this.swipeHandler(); + } + + canGoPrevious() { + return this.index > 0; + } + + canGoNext() { + return this.index + this.PER_PAGE < this.TOTAL; + } + + autoPlay() { + this.interval = setInterval(() => { + if (!this.canGoNext()) this.index = -1; + this.setIndex(this.canGoNext() ? 1 : -1); + }, this.INTERVAL_DURATION); + } + + swipeHandler() { + Object.entries(this.SWIPE_EVENTS).forEach(([action, types]) => { + types.forEach((type) => { + this.sliderBox.addEventListener( + type, + (event) => { + if (typeof this[action] === "function") this[action](event); + }, + { passive: ["touchstart", "touchmove"].includes(type) }, + ); + }); + }); + } + + startSwipe(event) { + this.setSwiping(true); + this.setTransitionDuration(0); + + this.startX = event.touches ? event.touches[0].clientX : event.clientX; + this.currentX = this.startX; + } + + onSwipe(event) { + if (!this.isSwiping) return; + + this.currentX = event.touches ? event.touches[0].clientX : event.clientX; + const deltaX = this.currentX - this.startX; + + if ( + (!this.canGoPrevious() && deltaX > 0) || + (!this.canGoNext() && deltaX < 0) + ) { + this.isBoundaryAllowed = false; + this.move(deltaX / 4); + } else { + this.isBoundaryAllowed = true; + this.move(deltaX); + } + } + + endSwipe() { + if (!this.isSwiping) return; + + const deltaX = this.currentX - this.startX; + const itemWidth = this.sliderBox.offsetWidth / this.PER_PAGE; + const threshold = itemWidth / 4; + const isValidSwipe = this.isBoundaryAllowed && Math.abs(deltaX) > threshold; + + isValidSwipe ? this.setIndex(deltaX > 0 ? -1 : 1) : this.move(); + + this.setSwiping(false); + this.setTransitionDuration(this.SPEED); + } + + move(offset = 0) { + const translateX = `calc(-${(100 / this.PER_PAGE) * this.index}% - ${ + (this.GAP / this.PER_PAGE) * this.index + }px + ${offset}px)`; + this.slider.style.transform = `translateX(${translateX})`; + } + + setSwiping(state) { + this.isSwiping = state; + } + + setArrows() { + this.rightArrow.disabled = !this.canGoNext(); + this.leftArrow.disabled = !this.canGoPrevious(); + } + + setTransitionDuration(duration) { + this.slider.style.transitionDuration = `${duration}ms`; + } + + setIndex(direction) { + const previousIndex = this.index; + this.index = Math.max(0, Math.min(this.TOTAL - 1, this.index + direction)); + + if (this.index !== previousIndex) { + this.move(); + this.setArrows(); + } + } +} + +customElements.define("yc-slider", Slider); diff --git a/themes/origins/styles/base/_common.scss b/themes/origins/styles/base/_common.scss index 0221e54..042fd66 100644 --- a/themes/origins/styles/base/_common.scss +++ b/themes/origins/styles/base/_common.scss @@ -32,8 +32,8 @@ button, } &.icon { padding: 12px; - background-color: transparent; color: var(--color-primary); + background-color: var(--color-secondary); border: 1px solid var(--color-gray-300); &[data-size$="sm"] { padding: 8px; diff --git a/themes/origins/styles/component-slider.scss b/themes/origins/styles/component-slider.scss new file mode 100644 index 0000000..4a3f80a --- /dev/null +++ b/themes/origins/styles/component-slider.scss @@ -0,0 +1,53 @@ +yc-slider { + display: grid; + position: relative; + margin: 2em 10%; + height: calc(var(--slider-height) * 1px); + .slider-area { + display: flex; + .slider-box { + touch-action: pan-y; + user-select: none; + overflow: hidden; + height: 100%; + width: 100%; + .slider-inner { + height: 100%; + display: flex; + gap: calc(var(--slider-gap) * 1px); + transition: transform cubic-bezier(0.25, 1, 0.5, 1); + transition-duration: calc(var(--slider-speed) * 1ms); + .slider-item { + user-select: none; + min-width: calc( + 100% / var(--slider-per-page) - (var(--slider-gap) * 1px) + + (var(--slider-gap) / var(--slider-per-page) * 1px) + ); + } + } + } + } + .slider-arrows { + position: absolute; + display: flex; + align-items: center; + gap: var(--gap-lg); + position: absolute; + inset-block-end: 1em; + translate: -50% -50%; + inset-inline-start: 50%; + button { + padding: 8px 15px; + color: var(--color-primary); + background-color: var(--color-secondary); + border-radius: var(--radius-sm); + box-shadow: + inset rgba(50, 50, 93, 0.1) 0px 2px 5px -1px, + rgba(0, 0, 0, 0.2) 0px 1px 3px -1px; + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + } +}