diff --git a/components.js b/components.js new file mode 100644 index 0000000..2cbf966 --- /dev/null +++ b/components.js @@ -0,0 +1,3292 @@ +//Button +const smButton = document.createElement('template') +smButton.innerHTML = ` + +`; +customElements.define('sm-button', + class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smButton.content.cloneNode(true)) + } + + get disabled() { + return this.isDisabled + } + + set disabled(value) { + if (value && !this.isDisabled) { + this.isDisabled = true + this.setAttribute('disabled', '') + this.button.removeAttribute('tabindex') + } else if (!value && this.isDisabled) { + this.isDisabled = false + this.removeAttribute('disabled') + } + } + + dispatch() { + if (this.isDisabled) { + this.dispatchEvent(new CustomEvent('disabled', { + bubbles: true, + composed: true + })) + } else { + this.dispatchEvent(new CustomEvent('clicked', { + bubbles: true, + composed: true + })) + } + } + + connectedCallback() { + this.isDisabled = false + this.button = this.shadowRoot.querySelector('.button') + if (this.hasAttribute('disabled') && !this.isDisabled) + this.isDisabled = true + this.addEventListener('click', (e) => { + this.dispatch() + }) + } + }) + +//Input +const smInput = document.createElement('template') +smInput.innerHTML = ` + +
+ +
+
+`; +customElements.define('sm-input', + class extends HTMLElement { + + static formAssociated = true; + + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smInput.content.cloneNode(true)) + } + + static get observedAttributes() { + return ['placeholder'] + } + + get value() { + return this.shadowRoot.querySelector('input').value + } + + set value(val) { + this.shadowRoot.querySelector('input').value = val; + this.checkInput() + this.fireEvent() + } + + get placeholder() { + return this.getAttribute('placeholder') + } + + set placeholder(val) { + this.setAttribute('placeholder', val) + } + + get type() { + return this.getAttribute('type') + } + + get isValid() { + return this.shadowRoot.querySelector('input').checkValidity() + } + + get validity() { + return this.shadowRoot.querySelector('input').validity + } + + set disabled(value) { + if (value) + this.shadowRoot.querySelector('.input').classList.add('disabled') + else + this.shadowRoot.querySelector('.input').classList.remove('disabled') + } + set readOnly(value) { + if (value) { + this.shadowRoot.querySelector('input').setAttribute('readonly', '') + this.shadowRoot.querySelector('.input').classList.add('readonly') + } else { + this.shadowRoot.querySelector('input').removeAttribute('readonly') + this.shadowRoot.querySelector('.input').classList.remove('readonly') + } + } + + setValidity = (message) => { + this.feedbackText.textContent = message + } + + showValidity = () => { + this.feedbackText.classList.remove('hide-completely') + } + + hideValidity = () => { + this.feedbackText.classList.add('hide-completely') + } + + focusIn = () => { + this.input.focus() + } + + focusOut = () => { + this.input.blur() + } + + fireEvent = () => { + let event = new Event('input', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(event); + } + + checkInput = (e) => { + if (!this.readonly) { + if (this.input.value !== '') { + this.clearBtn.classList.remove('hide') + } else { + this.clearBtn.classList.add('hide') + } + } + if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder') === '') return; + if (this.input.value !== '') { + if (this.animate) + this.inputParent.classList.add('animate-label') + else + this.label.classList.add('hide') + } else { + if (this.animate) + this.inputParent.classList.remove('animate-label') + else + this.label.classList.remove('hide') + } + } + + + connectedCallback() { + this.inputParent = this.shadowRoot.querySelector('.input') + this.clearBtn = this.shadowRoot.querySelector('.clear') + this.label = this.shadowRoot.querySelector('.label') + this.feedbackText = this.shadowRoot.querySelector('.feedback-text') + this.valueChanged = false; + this.readonly = false + this.isNumeric = false + this.min + this.max + this.animate = this.hasAttribute('animate') + this.input = this.shadowRoot.querySelector('input') + this.shadowRoot.querySelector('.label').textContent = this.getAttribute('placeholder') + if (this.hasAttribute('value')) { + this.input.value = this.getAttribute('value') + this.checkInput() + } + if (this.hasAttribute('required')) { + this.input.setAttribute('required', '') + } + if (this.hasAttribute('min')) { + let minValue = this.getAttribute('min') + this.input.setAttribute('min', minValue) + this.min = parseInt(minValue) + } + if (this.hasAttribute('max')) { + let maxValue = this.getAttribute('max') + this.input.setAttribute('max', maxValue) + this.max = parseInt(maxValue) + } + if (this.hasAttribute('minlength')) { + const minValue = this.getAttribute('minlength') + this.input.setAttribute('minlength', minValue) + } + if (this.hasAttribute('maxlength')) { + const maxValue = this.getAttribute('maxlength') + this.input.setAttribute('maxlength', maxValue) + } + if (this.hasAttribute('step')) { + const steps = this.getAttribute('step') + this.input.setAttribute('step', steps) + } + if (this.hasAttribute('pattern')) { + this.input.setAttribute('pattern', this.getAttribute('pattern')) + } + if (this.hasAttribute('readonly')) { + this.input.setAttribute('readonly', '') + this.readonly = true + } + if (this.hasAttribute('disabled')) { + this.inputParent.classList.add('disabled') + } + if (this.hasAttribute('error-text')) { + this.feedbackText.textContent = this.getAttribute('error-text') + } + if (this.hasAttribute('type')) { + if (this.getAttribute('type') === 'number') { + this.input.setAttribute('inputmode', 'numeric') + this.input.setAttribute('type', 'number') + this.isNumeric = true + } else + this.input.setAttribute('type', this.getAttribute('type')) + } else + this.input.setAttribute('type', 'text') + this.input.addEventListener('input', e => { + this.checkInput(e) + }) + this.clearBtn.addEventListener('click', e => { + this.value = '' + }) + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue !== newValue) { + if (name === 'placeholder') { + this.shadowRoot.querySelector('.label').textContent = newValue; + this.setAttribute('aria-label', newValue); + } + } + } + }) + +//textarea +const smTextarea = document.createElement('template') +smTextarea.innerHTML = ` + + +`; +customElements.define('sm-textarea', + class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smTextarea.content.cloneNode(true)) + this.textarea = this.shadowRoot.querySelector('textarea') + } + get value() { + return this.textarea.value + } + set value(val) { + this.textarea.value = val; + this.textareaBox.dataset.value = val + this.checkInput() + this.fireEvent() + } + focusIn = () => { + this.textarea.focus() + } + fireEvent() { + let event = new Event('input', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(event); + } + checkInput = () => { + if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder') === '') + return; + if (this.textarea.value !== '') { + this.placeholder.classList.add('hide') + } else { + this.placeholder.classList.remove('hide') + } + } + connectedCallback() { + this.textareaBox = this.shadowRoot.querySelector('.textarea') + this.placeholder = this.shadowRoot.querySelector('.placeholder') + + if(this.hasAttribute('placeholder')) + this.placeholder.textContent = this.getAttribute('placeholder') + + if (this.hasAttribute('value')) { + this.textarea.value = this.getAttribute('value') + this.checkInput() + } + if (this.hasAttribute('required')) { + this.textarea.setAttribute('required', '') + } + if (this.hasAttribute('readonly')) { + this.textarea.setAttribute('readonly', '') + } + if (this.hasAttribute('rows')) { + this.textarea.setAttribute('rows', this.getAttribute('rows')) + } + this.textarea.addEventListener('input', e => { + this.textareaBox.dataset.value = this.textarea.value + this.checkInput() + }) + } + }) + +// tab +const smTab = document.createElement('template') +smTab.innerHTML = ` + +
+ +
+`; + +customElements.define('sm-tab', class extends HTMLElement { + constructor() { + super() + this.shadow = this.attachShadow({ + mode: 'open' + }).append(smTab.content.cloneNode(true)) + } +}) + +//chcekbox + +const smCheckbox = document.createElement('template') +smCheckbox.innerHTML = ` + +` +customElements.define('sm-checkbox', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smCheckbox.content.cloneNode(true)) + + this.checkbox = this.shadowRoot.querySelector('.checkbox'); + this.input = this.shadowRoot.querySelector('input') + + this.isChecked = false + this.isDisabled = false + } + + static get observedAttributes() { + return ['disabled', 'checked'] + } + + get disabled() { + return this.isDisabled + } + + set disabled(val) { + if (val) { + this.setAttribute('disabled', '') + } else { + this.removeAttribute('disabled') + } + } + + get checked() { + return this.isChecked + } + + set checked(value) { + if (value) { + this.setAttribute('checked', '') + } + else { + this.removeAttribute('checked') + } + } + + set value(val) { + this.val = val + this.setAttribute('value', value) + } + + get value() { + return getAttribute('value') + } + + dispatch = () => { + this.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true + })) + } + handleKeyup = e => { + if ((e.code === "Enter" || e.code === "Space") && this.isDisabled == false) { + if (this.hasAttribute('checked')) { + this.input.checked = false + this.removeAttribute('checked') + } + else { + this.input.checked = true + this.setAttribute('checked', '') + } + } + } + handleChange = e => { + if (this.input.checked) { + this.setAttribute('checked', '') + } + else { + this.removeAttribute('checked') + } + } + + connectedCallback() { + this.val = '' + this.addEventListener('keyup', this.handleKeyup) + this.input.addEventListener('change', this.handleChange) + } + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue !== newValue) { + if (name === 'disabled') { + if (newValue === 'true') { + this.isDisabled = true + } else { + this.isDisabled = false + } + } + else if (name === 'checked') { + if (this.hasAttribute('checked')) { + this.isChecked = true + this.input.checked = true + } + else { + this.input.checked = false + this.isChecked = false + } + this.dispatch() + } + } + } + disconnectedCallback() { + this.removeEventListener('keyup', this.handleKeyup) + this.removeEventListener('change', this.handleChange) + } +}) + +//switch + +const smSwitch = document.createElement('template') +smSwitch.innerHTML = ` + +` + +customElements.define('sm-switch', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smSwitch.content.cloneNode(true)) + this.switch = this.shadowRoot.querySelector('.switch'); + this.input = this.shadowRoot.querySelector('input') + this.isChecked = false + this.isDisabled = false + } + + static get observedAttributes() { + return ['disabled', 'checked'] + } + + get disabled() { + return this.isDisabled + } + + set disabled(val) { + if (val) { + this.setAttribute('disabled', '') + } else { + this.removeAttribute('disabled') + } + } + + get checked() { + return this.isChecked + } + + set checked(value) { + if (value) { + this.setAttribute('checked', '') + } else { + this.removeAttribute('checked') + } + } + + dispatch = () => { + this.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true + })) + } + + connectedCallback() { + this.addEventListener('keyup', e => { + if ((e.code === "Enter" || e.code === "Space") && !this.isDisabled) { + this.input.click() + } + }) + this.input.addEventListener('click', e => { + if (this.input.checked) + this.checked = true + else + this.checked = false + this.dispatch() + }) + } + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue !== newValue) { + if (name === 'disabled') { + if (this.hasAttribute('disabled')) { + this.disabled = true + } + else { + this.disabled = false + } + } + else if (name === 'checked') { + if (this.hasAttribute('checked')) { + this.isChecked = true + this.input.checked = true + } + else { + this.isChecked = false + this.input.checked = false + } + } + } + } + +}) + +// select +const smSelect = document.createElement('template') +smSelect.innerHTML = ` + +
+
+
+ + + +
+
+ +
+
`; +customElements.define('sm-select', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smSelect.content.cloneNode(true)) + } + static get observedAttributes() { + return ['value'] + } + get value() { + return this.getAttribute('value') + } + set value(val) { + this.setAttribute('value', val) + } + + collapse() { + this.optionList.animate(this.slideUp, this.animationOptions) + this.optionList.classList.add('hide') + this.chevron.classList.remove('rotate') + this.open = false + } + connectedCallback() { + this.availableOptions + this.optionList = this.shadowRoot.querySelector('.options') + this.chevron = this.shadowRoot.querySelector('.toggle') + let slot = this.shadowRoot.querySelector('.options slot'), + selection = this.shadowRoot.querySelector('.selection'), + previousOption + this.open = false; + this.slideDown = [{ + transform: `translateY(-0.5rem)` + }, + { + transform: `translateY(0)` + } + ], + this.slideUp = [{ + transform: `translateY(0)` + }, + { + transform: `translateY(-0.5rem)` + } + ], + this.animationOptions = { + duration: 300, + fill: "forwards", + easing: 'ease' + } + selection.addEventListener('click', e => { + if (!this.open) { + this.optionList.classList.remove('hide') + this.optionList.animate(this.slideDown, this.animationOptions) + this.chevron.classList.add('rotate') + this.open = true + } else { + this.collapse() + } + }) + selection.addEventListener('keydown', e => { + if (e.code === 'ArrowDown' || e.code === 'ArrowRight') { + e.preventDefault() + this.availableOptions[0].focus() + } + if (e.code === 'Enter' || e.code === 'Space') + if (!this.open) { + this.optionList.classList.remove('hide') + this.optionList.animate(this.slideDown, this.animationOptions) + this.chevron.classList.add('rotate') + this.open = true + } else { + this.collapse() + } + }) + this.optionList.addEventListener('keydown', e => { + if (e.code === 'ArrowUp' || e.code === 'ArrowRight') { + e.preventDefault() + if (document.activeElement.previousElementSibling) { + document.activeElement.previousElementSibling.focus() + } + } + if (e.code === 'ArrowDown' || e.code === 'ArrowLeft') { + e.preventDefault() + if (document.activeElement.nextElementSibling) + document.activeElement.nextElementSibling.focus() + } + }) + this.addEventListener('optionSelected', e => { + if (previousOption !== e.target) { + this.setAttribute('value', e.detail.value) + this.shadowRoot.querySelector('.option-text').textContent = e.detail.text; + this.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true, + detail: { + value: e.detail.value + } + })) + if (previousOption) { + previousOption.classList.remove('check-selected') + } + previousOption = e.target; + } + if (!e.detail.switching) + this.collapse() + + e.target.classList.add('check-selected') + }) + slot.addEventListener('slotchange', e => { + this.availableOptions = slot.assignedElements() + if (this.availableOptions[0]) { + let firstElement = this.availableOptions[0]; + previousOption = firstElement; + firstElement.classList.add('check-selected') + this.setAttribute('value', firstElement.getAttribute('value')) + this.shadowRoot.querySelector('.option-text').textContent = firstElement.textContent + this.availableOptions.forEach((element, index) => { + element.setAttribute('data-rank', index + 1); + element.setAttribute('tabindex', "0"); + }) + } + }); + document.addEventListener('mousedown', e => { + if (!this.contains(e.target) && this.open) { + this.collapse() + } + }) + } +}) + +// option +const smOption = document.createElement('template') +smOption.innerHTML = ` + +
+ + + + +
`; +customElements.define('sm-option', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smOption.content.cloneNode(true)) + } + + sendDetails(switching) { + let optionSelected = new CustomEvent('optionSelected', { + bubbles: true, + composed: true, + detail: { + text: this.textContent, + value: this.getAttribute('value'), + switching: switching + } + }) + this.dispatchEvent(optionSelected) + } + + connectedCallback() { + let validKey = [ + 'ArrowUp', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight' + ] + this.addEventListener('click', e => { + this.sendDetails() + }) + this.addEventListener('keyup', e => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + this.sendDetails(false) + } + if (validKey.includes(e.code)) { + e.preventDefault() + this.sendDetails(true) + } + }) + if (this.hasAttribute('default')) { + setTimeout(() => { + this.sendDetails() + }, 0); + } + } +}) + +// select +const smStripSelect = document.createElement('template') +smStripSelect.innerHTML = ` + +
+
+ + Previous + + +
+ +
+ + Next + + +
+
`; +customElements.define('sm-strip-select', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smStripSelect.content.cloneNode(true)) + } + static get observedAttributes() { + return ['value'] + } + get value() { + return this.getAttribute('value') + } + set value(val) { + this.setAttribute('value', val) + } + scrollLeft = () => { + this.select.scrollBy({ + top: 0, + left: -this.scrollDistance, + behavior: 'smooth' + }) + } + + scrollRight = () => { + this.select.scrollBy({ + top: 0, + left: this.scrollDistance, + behavior: 'smooth' + }) + } + connectedCallback() { + let previousOption, + slot = this.shadowRoot.querySelector('slot'); + this.selectContainer = this.shadowRoot.querySelector('.select-container') + this.select = this.shadowRoot.querySelector('.select') + this.nextArrow = this.shadowRoot.querySelector('.next-item') + this.previousArrow = this.shadowRoot.querySelector('.previous-item') + this.nextGradient = this.shadowRoot.querySelector('.right') + this.previousGradient = this.shadowRoot.querySelector('.left') + this.selectOptions + this.scrollDistance = this.selectContainer.getBoundingClientRect().width + const firstElementObserver = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + this.previousArrow.classList.add('hide') + this.previousGradient.classList.add('hide') + } else { + this.previousArrow.classList.remove('hide') + this.previousGradient.classList.remove('hide') + } + }, { + root: this.selectContainer, + threshold: 0.95 + }) + const lastElementObserver = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + this.nextArrow.classList.add('hide') + this.nextGradient.classList.add('hide') + } else { + this.nextArrow.classList.remove('hide') + this.nextGradient.classList.remove('hide') + } + }, { + root: this.selectContainer, + threshold: 0.95 + }) + + const selectObserver = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + this.scrollDistance = this.selectContainer.getBoundingClientRect().width + } + }) + + selectObserver.observe(this.selectContainer) + this.addEventListener('optionSelected', e => { + if (previousOption === e.target) return; + if (previousOption) + previousOption.classList.remove('active') + e.target.classList.add('active') + e.target.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest' + }) + this.setAttribute('value', e.detail.value) + this.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true + })) + previousOption = e.target; + }) + slot.addEventListener('slotchange', e => { + this.selectOptions = slot.assignedElements() + firstElementObserver.observe(this.selectOptions[0]) + lastElementObserver.observe(this.selectOptions[this.selectOptions.length - 1]) + if (this.selectOptions[0]) { + let firstElement = this.selectOptions[0]; + this.setAttribute('value', firstElement.getAttribute('value')) + firstElement.classList.add('active') + previousOption = firstElement; + } + }); + this.nextArrow.addEventListener('click', this.scrollRight) + this.previousArrow.addEventListener('click', this.scrollLeft) + } + + disconnectedCallback() { + this.nextArrow.removeEventListener('click', this.scrollRight) + this.previousArrow.removeEventListener('click', this.scrollLeft) + } +}) + +// option +const smStripOption = document.createElement('template') +smStripOption.innerHTML = ` + +
+ +
`; +customElements.define('sm-strip-option', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smStripOption.content.cloneNode(true)) + } + sendDetails() { + let optionSelected = new CustomEvent('optionSelected', { + bubbles: true, + composed: true, + detail: { + text: this.textContent, + value: this.getAttribute('value') + } + }) + this.dispatchEvent(optionSelected) + } + + connectedCallback() { + this.addEventListener('click', e => { + this.sendDetails() + }) + this.addEventListener('keyup', e => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + this.sendDetails(false) + } + }) + if (this.hasAttribute('default')) { + setTimeout(() => { + this.sendDetails() + }, 0); + } + } +}) + +//popup +const smPopup = document.createElement('template') +smPopup.innerHTML = ` + + +`; +customElements.define('sm-popup', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smPopup.content.cloneNode(true)) + + this.allowClosing = false + this.isOpen = false + } + + get open() { + return this.isOpen + } + + resumeScrolling = () => { + const scrollY = document.body.style.top; + window.scrollTo(0, parseInt(scrollY || '0') * -1); + setTimeout(() => { + document.body.style.overflow = 'auto'; + document.body.style.top= 'initial' + }, 300); + } + + show = (pinned, popupStack) => { + if (popupStack) + this.popupStack = popupStack + if (this.popupStack && !this.hasAttribute('open')) { + this.popupStack.push({ + popup: this, + permission: pinned + }) + if (this.popupStack.items.length > 1) { + this.popupStack.items[this.popupStack.items.length - 2].popup.classList.add('stacked') + } + this.dispatchEvent( + new CustomEvent("popupopened", { + bubbles: true, + detail: { + popup: this, + popupStack: this.popupStack + } + }) + ) + this.setAttribute('open', '') + this.pinned = pinned + this.isOpen = true + } + this.popupContainer.classList.remove('hide') + this.popup.style.transform = 'none'; + document.body.style.overflow = 'hidden'; + document.body.style.top= `-${window.scrollY}px` + return this.popupStack + } + hide = () => { + if (window.innerWidth < 640) + this.popup.style.transform = 'translateY(100%)'; + else + this.popup.style.transform = 'translateY(3rem)'; + this.popupContainer.classList.add('hide') + this.removeAttribute('open') + if (typeof this.popupStack !== 'undefined') { + this.popupStack.pop() + if (this.popupStack.items.length) { + this.popupStack.items[this.popupStack.items.length - 1].popup.classList.remove('stacked') + } else { + this.resumeScrolling() + } + } else { + this.resumeScrolling() + } + + if (this.inputFields.length) { + setTimeout(() => { + this.inputFields.forEach(field => { + if (field.type === 'radio' || field.tagName === 'SM-CHECKBOX') + field.checked = false + if (field.tagName === 'SM-INPUT' || field.tagName === 'TEXTAREA'|| field.tagName === 'SM-TEXTAREA') + field.value = '' + }) + }, 300); + } + setTimeout(() => { + this.dispatchEvent( + new CustomEvent("popupclosed", { + bubbles: true, + detail: { + popup: this, + popupStack: this.popupStack + } + }) + ) + this.isOpen = false + }, 300); + } + + handleTouchStart = (e) => { + this.touchStartY = e.changedTouches[0].clientY + this.popup.style.transition = 'transform 0.1s' + this.touchStartTime = e.timeStamp + } + + handleTouchMove = (e) => { + if (this.touchStartY < e.changedTouches[0].clientY) { + this.offset = e.changedTouches[0].clientY - this.touchStartY; + this.touchEndAnimataion = window.requestAnimationFrame(() => this.movePopup()) + } + /*else { + this.offset = this.touchStartY - e.changedTouches[0].clientY; + this.popup.style.transform = `translateY(-${this.offset}px)` + }*/ + } + + handleTouchEnd = (e) => { + this.touchEndTime = e.timeStamp + cancelAnimationFrame(this.touchEndAnimataion) + this.touchEndY = e.changedTouches[0].clientY + this.popup.style.transition = 'transform 0.3s' + this.threshold = this.popup.getBoundingClientRect().height * 0.3 + if (this.touchEndTime - this.touchStartTime > 200) { + if (this.touchEndY - this.touchStartY > this.threshold) { + if (this.pinned) { + this.show() + return + } else + this.hide() + } else { + this.show() + } + } else { + if (this.touchEndY > this.touchStartY) + if (this.pinned) { + this.show() + return + } + else + this.hide() + } + } + + movePopup = () => { + this.popup.style.transform = `translateY(${this.offset}px)` + } + + connectedCallback() { + this.pinned = false + this.popupStack + this.popupContainer = this.shadowRoot.querySelector('.popup-container') + this.popup = this.shadowRoot.querySelector('.popup') + this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot') + this.offset + this.popupHeader = this.shadowRoot.querySelector('.popup-top') + this.touchStartY = 0 + this.touchEndY = 0 + this.touchStartTime = 0 + this.touchEndTime = 0 + this.touchEndAnimataion; + this.threshold = this.popup.getBoundingClientRect().height * 0.3 + + if (this.hasAttribute('open')) + this.show() + this.popupContainer.addEventListener('mousedown', e => { + if (e.target === this.popupContainer && !this.pinned) { + if (this.pinned) { + this.show() + return + } else + this.hide() + } + }) + + this.popupBodySlot.addEventListener('slotchange', () => { + setTimeout(() => { + this.threshold = this.popup.getBoundingClientRect().height * 0.3 + }, 200); + this.inputFields = this.querySelectorAll('sm-input', 'sm-checkbox', 'textarea', 'sm-textarea', 'radio') + }) + + this.popupHeader.addEventListener('touchstart', (e) => { this.handleTouchStart(e) }, {passive: true}) + this.popupHeader.addEventListener('touchmove', (e) => {this.handleTouchMove(e)}, {passive: true}) + this.popupHeader.addEventListener('touchend', (e) => {this.handleTouchEnd(e)}, {passive: true}) + } + disconnectedCallback() { + this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, {passive: true}) + this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, {passive: true}) + this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, {passive: true}) + } +}) + +//carousel + +const smCarousel = document.createElement('template') +smCarousel.innerHTML = ` + + +`; + +customElements.define('sm-carousel', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smCarousel.content.cloneNode(true)) + + this.isAutoPlaying = false + this.autoPlayInterval = 5000 + this.autoPlayTimeout + this.initialTimeout + this.activeSlideNum = 0 + this.carouselItems + this.indicators + this.showIndicator = false + this.carousel = this.shadowRoot.querySelector('.carousel') + this.carouselContainer = this.shadowRoot.querySelector('.carousel-container') + this.carouselSlot = this.shadowRoot.querySelector('slot') + this.nextArrow = this.shadowRoot.querySelector('.carousel__button--right') + this.previousArrow = this.shadowRoot.querySelector('.carousel__button--left') + this.indicatorsContainer = this.shadowRoot.querySelector('.indicators') + } + + static get observedAttributes() { + return ['indicator', 'autoplay', 'interval'] + } + + scrollLeft = () => { + this.carousel.scrollBy({ + left: -this.scrollDistance, + behavior: 'smooth' + }) + } + + scrollRight = () => { + this.carousel.scrollBy({ + left: this.scrollDistance, + behavior: 'smooth' + }) + } + + handleIndicatorClick = (e) => { + if (e.target.closest('.dot')) { + const slideNum = parseInt(e.target.closest('.dot').dataset.rank) + if (this.activeSlideNum !== slideNum) { + this.showSlide(slideNum) + } + } + } + + showSlide = (slideNum) => { + this.carousel.scrollTo({ + left: (this.carouselItems[slideNum].getBoundingClientRect().left - this.carousel.getBoundingClientRect().left + this.carousel.scrollLeft), + behavior: 'smooth' + }) + } + + nextSlide = () => { + if (!this.carouselItems) return + let showSlideNo = (this.activeSlideNum + 1) < this.carouselItems.length ? this.activeSlideNum + 1 : 0 + this.showSlide(showSlideNo) + } + + autoPlay = () => { + this.nextSlide() + if (this.isAutoPlaying) { + this.autoPlayTimeout = setTimeout(() => { + this.autoPlay() + }, this.autoPlayInterval); + } + } + + startAutoPlay = () => { + this.setAttribute('autoplay', '') + } + + stopAutoPlay = () => { + this.removeAttribute('autoplay') + } + + connectedCallback() { + this.scrollDistance = this.carouselContainer.getBoundingClientRect().width / 3 + let frag = document.createDocumentFragment(); + if (this.hasAttribute('indicator')) + this.showIndicator = true + + + let firstVisible = false, + lastVisible = false + const allElementsObserver = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (this.showIndicator) { + const activeRank = parseInt(entry.target.dataset.rank) + if (entry.isIntersecting) { + this.indicators[activeRank].classList.add('active') + this.activeSlideNum = activeRank + } + else + this.indicators[activeRank].classList.remove('active') + } + if (!entry.target.previousElementSibling) + if (entry.isIntersecting) { + this.previousArrow.classList.remove('expand') + firstVisible = true + } + else { + this.previousArrow.classList.add('expand') + firstVisible = false + } + if (!entry.target.nextElementSibling) + if (entry.isIntersecting) { + this.nextArrow.classList.remove('expand') + lastVisible = true + } + else { + this.nextArrow.classList.add('expand') + lastVisible = false + } + if (firstVisible && lastVisible) + this.indicatorsContainer.classList.add('hide') + else + this.indicatorsContainer.classList.remove('hide') + }) + }, { + root: this.carouselContainer, + threshold: 0.9 + }) + + const carouselObserver = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + this.scrollDistance = this.carouselContainer.getBoundingClientRect().width / 3 + } + }) + + carouselObserver.observe(this.carouselContainer) + + this.carouselSlot.addEventListener('slotchange', e => { + this.carouselItems = this.carouselSlot.assignedElements() + this.carouselItems.forEach(item => allElementsObserver.observe(item)) + if (this.showIndicator) { + this.indicatorsContainer.innerHTML = `` + this.carouselItems.forEach((item, index) => { + let dot = document.createElement('div') + dot.classList.add('dot') + dot.dataset.rank = index + frag.append(dot) + item.dataset.rank = index + }) + this.indicatorsContainer.append(frag) + this.indicators = this.indicatorsContainer.children + } + }) + + this.addEventListener('keyup', e => { + if (e.code === 'ArrowLeft') + this.scrollRight() + else if (e.code === 'ArrowRight') + this.scrollRight() + }) + + this.nextArrow.addEventListener('click', this.scrollRight) + this.previousArrow.addEventListener('click', this.scrollLeft) + this.indicatorsContainer.addEventListener('click', this.handleIndicatorClick) + } + + async attributeChangedCallback(name, oldValue, newValue) { + if (oldValue !== newValue) { + if (name === 'indicator') { + if (this.hasAttribute('indicator')) + this.showIndicator = true + else + this.showIndicator = false + } + if (name === 'autoplay') { + if (this.hasAttribute('autoplay')) { + this.initialTimeout = setTimeout(() => { + this.isAutoPlaying = true + this.autoPlay() + }, this.autoPlayInterval); + } + else { + this.isAutoPlaying = false + clearTimeout(this.autoPlayTimeout) + clearTimeout(this.initialTimeout) + } + + } + if (name === 'interval') { + if (this.hasAttribute('interval') && this.getAttribute('interval').trim() !== '') { + this.autoPlayInterval = Math.abs(parseInt(this.getAttribute('interval').trim())) + } + else { + this.autoPlayInterval = 5000 + } + } + } + } + + disconnectedCallback() { + this.nextArrow.removeEventListener('click', this.scrollRight) + this.previousArrow.removeEventListener('click', this.scrollLeft) + this.indicatorsContainer.removeEventListener('click', this.handleIndicatorClick) + } +}) + +//notifications + +const smNotifications = document.createElement('template') +smNotifications.innerHTML = ` + +
+` + +customElements.define('sm-notifications', class extends HTMLElement { + constructor() { + super() + this.shadow = this.attachShadow({ + mode: 'open' + }).append(smNotifications.content.cloneNode(true)) + } + + handleTouchStart = (e) => { + this.notification = e.target.closest('.notification') + this.touchStartX = e.changedTouches[0].clientX + this.notification.style.transition = 'initial' + this.touchStartTime = e.timeStamp + } + + handleTouchMove = (e) => { + e.preventDefault() + if (this.touchStartX < e.changedTouches[0].clientX) { + this.offset = e.changedTouches[0].clientX - this.touchStartX; + this.touchEndAnimataion = requestAnimationFrame(this.movePopup) + } else { + this.offset = -(this.touchStartX - e.changedTouches[0].clientX); + this.touchEndAnimataion = requestAnimationFrame(this.movePopup) + } + } + + handleTouchEnd = (e) => { + this.notification.style.transition = 'transform 0.3s, opacity 0.3s' + this.touchEndTime = e.timeStamp + cancelAnimationFrame(this.touchEndAnimataion) + this.touchEndX = e.changedTouches[0].clientX + if (this.touchEndTime - this.touchStartTime > 200) { + if (this.touchEndX - this.touchStartX > this.threshold) { + this.removeNotification(this.notification) + } else if (this.touchStartX - this.touchEndX > this.threshold) { + this.removeNotification(this.notification, true) + } else { + this.resetPosition() + } + } else { + if (this.touchEndX > this.touchStartX) { + this.removeNotification(this.notification) + } else { + this.removeNotification(this.notification, true) + } + } + } + + movePopup = () => { + this.notification.style.transform = `translateX(${this.offset}px)` + } + + resetPosition = () => { + this.notification.style.transform = `translateX(0)` + } + + push = (messageBody, type, pinned) => { + let notification = document.createElement('div'), + composition = `` + notification.classList.add('notification') + if (pinned) + notification.classList.add('pinned') + if (type === 'error') { + composition += ` + + + + + ` + } else if (type === 'success') { + composition += ` + + + ` + } + composition += ` +

${messageBody}

+ + Close + + + ` + notification.innerHTML = composition + this.notificationPanel.prepend(notification) + if (window.innerWidth > 640) { + notification.animate([{ + transform: `translateX(1rem)`, + opacity: '0' + }, + { + transform: 'translateX(0)', + opacity: '1' + } + ], this.animationOptions).onfinish = () => { + notification.setAttribute('style', `transform: none;`); + } + } else { + notification.setAttribute('style', `transform: translateY(0); opacity: 1`) + } + notification.addEventListener('touchstart', this.handleTouchStart) + notification.addEventListener('touchmove', this.handleTouchMove) + notification.addEventListener('touchend', this.handleTouchEnd) + } + + removeNotification = (notification, toLeft) => { + if (!this.offset) + this.offset = 0; + + if (toLeft) + notification.animate([{ + transform: `translateX(${this.offset}px)`, + opacity: '1' + }, + { + transform: `translateX(-100%)`, + opacity: '0' + } + ], this.animationOptions).onfinish = () => { + notification.remove() + } + else { + notification.animate([{ + transform: `translateX(${this.offset}px)`, + opacity: '1' + }, + { + transform: `translateX(100%)`, + opacity: '0' + } + ], this.animationOptions).onfinish = () => { + notification.remove() + } + } + } + + clearAll = () => { + Array.from(this.notificationPanel.children).forEach(child => { + this.removeNotification(child) + }) + } + + connectedCallback() { + this.notificationPanel = this.shadowRoot.querySelector('.notification-panel') + this.animationOptions = { + duration: 300, + fill: "forwards", + easing: "ease" + } + this.fontSize = Number(window.getComputedStyle(document.body).getPropertyValue('font-size').match(/\d+/)[0]) + this.notification + this.offset + this.touchStartX = 0 + this.touchEndX = 0 + this.touchStartTime = 0 + this.touchEndTime = 0 + this.threshold = this.notificationPanel.getBoundingClientRect().width * 0.3 + this.touchEndAnimataion; + + this.notificationPanel.addEventListener('click', e => { + if (e.target.closest('.close'))( + this.removeNotification(e.target.closest('.notification')) + ) + }) + + const observer = new MutationObserver(mutationList => { + mutationList.forEach(mutation => { + if (mutation.type === 'childList') { + if (mutation.addedNodes.length) { + if (!mutation.addedNodes[0].classList.contains('pinned')) + setTimeout(() => { + this.removeNotification(mutation.addedNodes[0]) + }, 5000); + if (window.innerWidth > 640) + this.notificationPanel.style.padding = '1.5rem 0 3rem 1.5rem'; + else + this.notificationPanel.style.padding = '1rem 1rem 2rem 1rem'; + } else if (mutation.removedNodes.length && !this.notificationPanel.children.length) { + this.notificationPanel.style.padding = 0; + } + } + }) + }) + observer.observe(this.notificationPanel, { + attributes: true, + childList: true, + subtree: true + }) + } +}) + + + +// sm-menu +const smMenu = document.createElement('template') +smMenu.innerHTML = ` + +
+ +
+ +
+
`; +customElements.define('sm-menu', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smMenu.content.cloneNode(true)) + } + static get observedAttributes() { + return ['value'] + } + get value() { + return this.getAttribute('value') + } + set value(val) { + this.setAttribute('value', val) + } + expand = () => { + if (!this.open) { + this.optionList.classList.remove('hide') + this.optionList.classList.add('no-transformations') + this.open = true + this.icon.classList.add('focused') + this.availableOptions.forEach(option => { + option.setAttribute('tabindex', '0') + }) + } + } + collapse() { + if (this.open) { + this.open = false + this.icon.classList.remove('focused') + this.optionList.classList.add('hide') + this.optionList.classList.remove('no-transformations') + this.availableOptions.forEach(option => { + option.removeAttribute('tabindex') + }) + } + } + connectedCallback() { + this.availableOptions + this.containerDimensions + this.optionList = this.shadowRoot.querySelector('.options') + let slot = this.shadowRoot.querySelector('.options slot'), + menu = this.shadowRoot.querySelector('.menu') + this.icon = this.shadowRoot.querySelector('.icon') + this.open = false; + menu.addEventListener('click', e => { + if (!this.open) { + this.expand() + } else { + this.collapse() + } + }) + menu.addEventListener('keydown', e => { + if (e.code === 'ArrowDown' || e.code === 'ArrowRight') { + e.preventDefault() + this.availableOptions[0].focus() + } + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + if (!this.open) { + this.expand() + } else { + this.collapse() + } + } + }) + this.optionList.addEventListener('keydown', e => { + if (e.code === 'ArrowUp' || e.code === 'ArrowRight') { + e.preventDefault() + if (document.activeElement.previousElementSibling) { + document.activeElement.previousElementSibling.focus() + } + } + if (e.code === 'ArrowDown' || e.code === 'ArrowLeft') { + e.preventDefault() + if (document.activeElement.nextElementSibling) + document.activeElement.nextElementSibling.focus() + } + }) + this.optionList.addEventListener('click', e => { + this.collapse() + }) + slot.addEventListener('slotchange', e => { + this.availableOptions = slot.assignedElements() + this.containerDimensions = this.optionList.getBoundingClientRect() + }); + window.addEventListener('mousedown', e => { + if (!this.contains(e.target) && e.button !== 2) { + this.collapse() + } + }) + } +}) + +// option +const smMenuOption = document.createElement('template') +smMenuOption.innerHTML = ` + +
+ +
`; +customElements.define('sm-menu-option', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smMenuOption.content.cloneNode(true)) + } + + connectedCallback() { + this.addEventListener('keyup', e => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault() + this.click() + } + }) + } +}) \ No newline at end of file diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..d764cbf --- /dev/null +++ b/css/main.css @@ -0,0 +1,508 @@ +* { + padding: 0; + margin: 0; + box-sizing: border-box; + font-family: "Inter", sans-serif; +} + +:root { + font-size: clamp(1rem, 1.2vmax, 3rem); +} + +html, body { + height: 100%; + scroll-behavior: smooth; +} + +body { + --accent-color: #2353FF; + --light-shade: rgba(var(--text-color), 0.06); + --text-color: 17, 17, 17; + --text-color-light: 100, 100, 100; + --foreground-color: 255, 255, 255; + --background-color: #F6f6f6; + --error-color: red; + --green: #00843b; + color: rgba(var(--text-color), 1); + background: var(--background-color); +} + +body[data-theme=dark] { + --accent-color: #2353FF; + --green: #13ff5a; + --text-color: 240, 240, 240; + --text-color-light: 170, 170, 170; + --foreground-color: 20, 20, 20; + --background-color: #0a0a0a; + --error-color: rgb(255, 106, 106); +} + +.full-bleed { + grid-column: 1/4; +} + +.h1 { + font-size: 2.5rem; +} + +.h2 { + font-size: 2rem; +} + +.h3 { + font-size: 1.4rem; +} + +.h4 { + font-size: 1rem; +} + +.h5 { + font-size: 0.8rem; +} + +.uppercase { + text-transform: uppercase; +} + +.capitalize { + text-transform: capitalize; +} + +p { + font-size: 0.8; + max-width: 70ch; + line-height: 1.5; + color: rgba(var(--text-color), 0.8); +} + +img { + object-fit: cover; +} + +a { + color: inherit; + text-decoration: none; +} +a:focus-visible { + box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset; +} + +button { + position: relative; + display: inline-flex; + overflow: hidden; + align-items: center; + background: none; + cursor: pointer; + outline: none; + color: inherit; + font-size: 0.9rem; + font-weight: 500; + border-radius: 0.2rem; + padding: 0.5rem 0.6rem; + -webkit-tap-highlight-color: transparent; + border: none; +} + +button:focus-visible { + outline: rgba(var(--text-color), 1) 0.1rem solid; +} + +a:any-link { + position: relative; + display: inline-flex; + align-items: center; + background: none; + cursor: pointer; + outline: none; + color: inherit; + font-weight: 500; + font-size: 0.8rem; + border-radius: 0.3rem; + padding: 0.4rem 0.6rem; + align-self: flex-start; + text-decoration: none; + -webkit-tap-highlight-color: transparent; + border: 1px solid rgba(var(--text-color), 0.8); +} + +a:any-link:focus-visible { + outline: rgba(var(--text-color), 1) 0.1rem solid; +} + +sm-input, +sm-textarea { + --border-radius: 2rem; + --background: rgba(var(--text-color), 0.06); +} + +sm-button { + --border-radius: 0.2rem; +} + +ul { + list-style: none; +} + +.flex { + display: flex; +} + +.grid { + display: grid; +} + +.hide { + opacity: 0; + pointer-events: none; +} + +.hide-completely { + display: none !important; +} + +.no-transformations { + transform: none !important; +} + +.overflow-ellipsis { + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.breakable { + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +.flex { + display: flex; +} + +.grid { + display: grid; +} + +.flow-column { + grid-auto-flow: column; +} + +.gap-0-5 { + gap: 0.5rem; +} + +.gap-1 { + gap: 1rem; +} + +.gap-1-5 { + gap: 1.5rem; +} + +.gap-2 { + gap: 2rem; +} + +.gap-3 { + gap: 3rem; +} + +.text-align-right { + text-align: right; +} + +.align-start { + align-items: flex-start; +} + +.align-center { + align-items: center; +} + +.text-center { + text-align: center; +} + +.justify-start { + justify-content: start; +} + +.justify-center { + justify-content: center; +} + +.justify-right { + margin-left: auto; +} + +.align-self-center { + align-self: center; +} + +.justify-self-center { + justify-self: center; +} + +.justify-self-start { + justify-self: start; +} + +.direction-column { + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.ripple { + position: absolute; + border-radius: 50%; + transform: scale(0); + background: rgba(var(--text-color), 0.16); + pointer-events: none; +} + +.interact { + position: relative; + overflow: hidden; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.observe-empty-state:empty { + display: none; +} + +.observe-empty-state:not(:empty) ~ .empty-state { + display: none; +} + +.icon { + width: 1.5rem; + height: 1.5rem; + fill: rgba(var(--text-color), 0.9); +} + +.page-layout { + position: relative; + display: grid; + grid-template-columns: 1rem 1fr 1rem; +} +.page-layout > * { + grid-column: 2/3; +} + +.popup__header { + display: grid; + gap: 0.5rem; + width: 100%; + padding: 1rem 1.5rem; + align-items: center; + grid-template-columns: auto 1fr; +} + +.popup__header__close { + padding: 0.5rem; + cursor: pointer; +} + +#loading_page, +#error_page { + position: relative; + display: grid; + height: 100%; + place-content: center; + justify-items: center; +} + +#main_header { + position: relative; + display: flex; + padding: 1rem; + align-items: center; + justify-content: space-between; + grid-template-columns: repeat(3, 1fr); +} + +#main_header__logo { + height: 1.8rem; + width: 1.8rem; +} + +.theme-switcher { + position: relative; + justify-self: flex-end; + width: 1.5rem; + height: 1.5rem; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} +.theme-switcher .icon { + position: absolute; + transition: transform 0.6s; +} + +.theme-switcher__checkbox { + display: none; +} +.theme-switcher__checkbox:checked ~ .moon-icon { + transform: scale(0) rotate(90deg); +} +.theme-switcher__checkbox:not(:checked) ~ .sun-icon { + transform: scale(0) rotate(-90deg); +} + +#search_section { + display: grid; + gap: 1rem; + padding: 3rem 0; + justify-content: flex-start; +} + +#torrent_container { + padding: 1.5rem 0; + display: grid; + gap: 0.5rem; + padding-bottom: 4rem; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); +} + +.torrent-card { + display: flex; + flex-direction: column; + cursor: pointer; + padding: 1rem; + border-radius: 0.3rem; + background-color: rgba(var(--foreground-color), 1); +} +.torrent-card__icon { + grid-area: icon; + display: flex; + padding: 0.6rem; + border-radius: 50%; + margin-bottom: 1.5rem; + align-items: center; + justify-content: center; + align-self: flex-start; + aspect-ratio: 1/1; + flex-shrink: 0; + background-color: var(--accent-color); +} +.torrent-card__icon .icon { + height: 1.6rem; + width: 1.6rem; + fill: white; +} +.torrent-card__title { + grid-area: title; + font-size: 1.2rem; + margin-bottom: 0.5rem; +} +.torrent-card__tags { + grid-area: tags; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 0.85rem; + margin-bottom: 1.5rem; + color: rgba(var(--text-color), 0.7); +} +.torrent-card__description { + font-size: 0.9rem; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + color: rgba(var(--text-color), 0.8); +} +.torrent-card__size { + font-weight: 500; +} +.torrent-card__download-button { + grid-area: button; + justify-self: flex-end; + margin-top: auto; + justify-content: center; + background-color: rgba(var(--text-color), 0.1); +} +.torrent-card__download-button .icon { + margin-right: 0.5rem; +} + +#torrent_popup { + --width: min(42rem, 100%); +} + +#main_footer { + padding: 3rem 0; + background-color: rgba(var(--text-color), 0.06); +} + +@media only screen and (max-width: 640px) { + .torrent-card { + display: grid; + gap: 0 1rem; + align-items: center; + grid-template-columns: auto 1fr; + grid-template-areas: "icon title" "icon tags" "icon button"; + } + .torrent-card__icon { + margin: 0; + } + .torrent-card__tags { + margin-bottom: 0; + } + .torrent-card__description { + display: none; + } + .torrent-card__download-button { + margin-top: 1rem; + } +} +@media only screen and (min-width: 640px) { + .page-layout { + grid-template-columns: 1fr 90vw 1fr; + } + + #main_header { + padding: 1.5rem; + } + + .torrent-card { + padding: 1.5rem; + min-height: 22rem; + } +} +@media only screen and (min-width: 1280px) { + .page-layout { + grid-template-columns: 1fr 90vw 1fr; + } + + .multi-form { + grid-template-columns: 1fr 1fr; + } +} +@media (any-hover: hover) { + ::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; + } + + ::-webkit-scrollbar-thumb { + background: rgba(var(--text-color), 0.3); + border-radius: 1rem; + } + ::-webkit-scrollbar-thumb:hover { + background: rgba(var(--text-color), 0.5); + } +} \ No newline at end of file diff --git a/css/main.min.css b/css/main.min.css new file mode 100644 index 0000000..9a07977 --- /dev/null +++ b/css/main.min.css @@ -0,0 +1 @@ +a:any-link,button{position:relative;display:inline-flex;outline:0;-webkit-tap-highlight-color:transparent;font-weight:500}.torrent-card__size,a:any-link,button{font-weight:500}a,a:any-link{color:inherit;text-decoration:none}.interact,.theme-switcher,a:any-link,button{-webkit-tap-highlight-color:transparent}.overflow-ellipsis,.torrent-card__tags{white-space:nowrap;text-overflow:ellipsis}*{padding:0;margin:0;box-sizing:border-box;font-family:Inter,sans-serif}:root{font-size:clamp(1rem,1.2vmax,3rem)}body,html{height:100%;scroll-behavior:smooth}body{--accent-color:#2353FF;--light-shade:rgba(var(--text-color), 0.06);--text-color:17,17,17;--text-color-light:100,100,100;--foreground-color:255,255,255;--background-color:#F6f6f6;--error-color:red;--green:#00843b;color:rgba(var(--text-color),1);background:var(--background-color)}body[data-theme=dark]{--accent-color:#2353FF;--green:#13ff5a;--text-color:240,240,240;--text-color-light:170,170,170;--foreground-color:20,20,20;--background-color:#0a0a0a;--error-color:rgb(255, 106, 106)}.full-bleed{grid-column:1/4}.h1{font-size:2.5rem}.h2{font-size:2rem}.h3{font-size:1.4rem}.h4{font-size:1rem}.h5{font-size:.8rem}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}p{font-size:.8;max-width:70ch;line-height:1.5;color:rgba(var(--text-color),.8)}img{object-fit:cover}a:focus-visible{box-shadow:0 0 0 .1rem rgba(var(--text-color),1) inset}button{overflow:hidden;align-items:center;background:0 0;cursor:pointer;color:inherit;font-size:.9rem;border-radius:.2rem;padding:.5rem .6rem;border:none}.torrent-card,a:any-link{cursor:pointer;border-radius:.3rem}button:focus-visible{outline:solid rgba(var(--text-color),1)}a:any-link{align-items:center;background:0 0;font-size:.8rem;padding:.4rem .6rem;align-self:flex-start;border:1px solid}a:any-link:focus-visible{outline:solid rgba(var(--text-color),1)}sm-input,sm-textarea{--border-radius:2rem;--background:rgba(var(--text-color), 0.06)}sm-button{--border-radius:0.2rem}ul{list-style:none}.hide{opacity:0;pointer-events:none}.hide-completely{display:none!important}.no-transformations{transform:none!important}.overflow-ellipsis{width:100%;overflow:hidden}.breakable{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.flex{display:flex}.grid{display:grid}.flow-column{grid-auto-flow:column}.gap-0-5{gap:.5rem}.gap-1{gap:1rem}.gap-1-5{gap:1.5rem}.gap-2{gap:2rem}.gap-3{gap:3rem}.text-align-right{text-align:right}.align-start{align-items:flex-start}.align-center{align-items:center}.text-center{text-align:center}.justify-start{justify-content:start}.justify-center{justify-content:center}.justify-right{margin-left:auto}.align-self-center{align-self:center}.justify-self-center{justify-self:center}.justify-self-start{justify-self:start}.direction-column,.torrent-card{flex-direction:column}.space-between{justify-content:space-between}.ripple{position:absolute;border-radius:50%;transform:scale(0);background:rgba(var(--text-color),.16);pointer-events:none}.interact{position:relative;overflow:hidden;cursor:pointer}.observe-empty-state:empty{display:none}.observe-empty-state:not(:empty)~.empty-state{display:none}.icon{width:1.5rem;height:1.5rem;fill:rgba(var(--text-color),.9)}.page-layout{position:relative;display:grid;grid-template-columns:1rem 1fr 1rem}.page-layout>*{grid-column:2/3}.popup__header{display:grid;gap:.5rem;width:100%;padding:1rem 1.5rem;align-items:center;grid-template-columns:auto 1fr}.popup__header__close{padding:.5rem;cursor:pointer}#error_page,#loading_page{position:relative;display:grid;height:100%;place-content:center;justify-items:center}#main_header{position:relative;display:flex;padding:1rem;align-items:center;justify-content:space-between;grid-template-columns:repeat(3,1fr)}#main_header__logo{height:1.8rem;width:1.8rem}.theme-switcher{position:relative;justify-self:flex-end;width:1.5rem;height:1.5rem;cursor:pointer}.theme-switcher .icon{position:absolute;transition:transform .6s}.theme-switcher__checkbox{display:none}.theme-switcher__checkbox:checked~.moon-icon{transform:scale(0) rotate(90deg)}.theme-switcher__checkbox:not(:checked)~.sun-icon{transform:scale(0) rotate(-90deg)}#search_section{display:grid;gap:1rem;padding:3rem 0;justify-content:flex-start}#torrent_container{padding:1.5rem 0;display:grid;gap:.5rem;padding-bottom:4rem;grid-template-columns:repeat(auto-fill,minmax(16rem,1fr))}.torrent-card{display:flex;padding:1rem;background-color:rgba(var(--foreground-color),1)}.torrent-card__icon{grid-area:icon;display:flex;padding:.6rem;border-radius:50%;margin-bottom:1.5rem;align-items:center;justify-content:center;align-self:flex-start;aspect-ratio:1/1;flex-shrink:0;background-color:var(--accent-color)}.torrent-card__icon .icon{height:1.6rem;width:1.6rem;fill:#fff}.torrent-card__title{grid-area:title;font-size:1.2rem;margin-bottom:.5rem}.torrent-card__tags{grid-area:tags;overflow:hidden;font-size:.85rem;margin-bottom:1.5rem;color:rgba(var(--text-color),.7)}.torrent-card__description{font-size:.9rem;overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;color:rgba(var(--text-color),.8)}.torrent-card__download-button{grid-area:button;justify-self:flex-end;margin-top:auto;justify-content:center;background-color:rgba(var(--text-color),.1)}.torrent-card__download-button .icon{margin-right:.5rem}#torrent_popup{--width:min(42rem, 100%)}#main_footer{padding:3rem 0;background-color:rgba(var(--text-color),.06)}@media only screen and (max-width:640px){.torrent-card{display:grid;gap:0 1rem;align-items:center;grid-template-columns:auto 1fr;grid-template-areas:"icon title" "icon tags" "icon button"}.torrent-card__icon{margin:0}.torrent-card__tags{margin-bottom:0}.torrent-card__description{display:none}.torrent-card__download-button{margin-top:1rem}}@media only screen and (min-width:640px){#main_header,.torrent-card{padding:1.5rem}.page-layout{grid-template-columns:1fr 90vw 1fr}.torrent-card{min-height:22rem}}@media only screen and (min-width:1280px){.page-layout{grid-template-columns:1fr 90vw 1fr}.multi-form{grid-template-columns:1fr 1fr}}@media (any-hover:hover){::-webkit-scrollbar{width:.5rem;height:.5rem}::-webkit-scrollbar-thumb{background:rgba(var(--text-color),.3);border-radius:1rem}::-webkit-scrollbar-thumb:hover{background:rgba(var(--text-color),.5)}} \ No newline at end of file diff --git a/css/main.scss b/css/main.scss new file mode 100644 index 0000000..8bcf40b --- /dev/null +++ b/css/main.scss @@ -0,0 +1,448 @@ +*{ + padding: 0; + margin: 0; + box-sizing: border-box; + font-family: 'Inter', sans-serif; +} +:root{ + font-size: clamp(1rem, 1.2vmax, 3rem); +} +html, body{ + height: 100%; + scroll-behavior: smooth; +} +body { + --accent-color: #2353FF; + --light-shade: rgba(var(--text-color), 0.06); + --text-color: 17, 17, 17; + --text-color-light: 100, 100, 100; + --foreground-color: 255, 255, 255; + --background-color: #F6f6f6; + --error-color: red; + --green: #00843b; + color: rgba(var(--text-color), 1); + background: var(--background-color); +} +body[data-theme='dark']{ + --accent-color: #2353FF; + --green: #13ff5a; + --text-color: 240, 240, 240; + --text-color-light: 170, 170, 170; + --foreground-color: 20, 20, 20; + --background-color: #0a0a0a; + --error-color: rgb(255, 106, 106); +} + +.full-bleed{ + grid-column: 1/4; +} +.h1{ + font-size: 2.5rem; +} +.h2{ + font-size: 2rem; +} +.h3{ + font-size: 1.4rem; +} +.h4{ + font-size: 1rem; +} +.h5{ + font-size: 0.8rem; +} + +.uppercase{ + text-transform: uppercase; +} +.capitalize{ + text-transform: capitalize; +} + +p { + font-size: 0.8; + max-width: 70ch; + line-height: 1.5; + color: rgba(var(--text-color), 0.8); +} +img{ + object-fit: cover; +} + +a{ + color: inherit; + text-decoration: none; + &:focus-visible{ + box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset; + } +} +button{ + position: relative; + display: inline-flex; + overflow: hidden; + align-items: center; + background: none; + cursor: pointer; + outline: none; + color: inherit; + font-size: 0.9rem; + font-weight: 500; + border-radius: 0.2rem; + padding: 0.5rem 0.6rem; + -webkit-tap-highlight-color: transparent; + border: none; +} +button:focus-visible{ + outline: rgba(var(--text-color), 1) 0.1rem solid; +} +a:any-link{ + position: relative; + display: inline-flex; + align-items: center; + background: none; + cursor: pointer; + outline: none; + color: inherit; + font-weight: 500; + font-size: 0.8rem; + border-radius: 0.3rem; + padding: 0.4rem 0.6rem; + align-self: flex-start; + text-decoration: none; + -webkit-tap-highlight-color: transparent; + border: 1px solid rgba(var(--text-color), 0.8); +} +a:any-link:focus-visible{ + outline: rgba(var(--text-color), 1) 0.1rem solid; +} +sm-input, +sm-textarea{ + --border-radius: 2rem; + --background: rgba(var(--text-color), 0.06); +} +sm-button{ + --border-radius: 0.2rem; +} +ul{ + list-style: none; +} +.flex{ + display: flex; +} +.grid{ + display: grid; +} +.hide{ + opacity: 0; + pointer-events: none; +} +.hide-completely{ + display: none !important; +} +.no-transformations{ + transform: none !important; +} +.overflow-ellipsis{ + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.breakable{ + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} +.flex{ + display: flex; +} +.grid{ + display: grid; +} +.flow-column{ + grid-auto-flow: column; +} +.gap-0-5{ + gap: 0.5rem; +} +.gap-1{ + gap: 1rem; +} +.gap-1-5{ + gap: 1.5rem; +} +.gap-2{ + gap: 2rem; +} +.gap-3{ + gap: 3rem; +} +.text-align-right{ + text-align: right; +} +.align-start{ + align-items: flex-start; +} +.align-center{ + align-items: center; +} +.text-center{ + text-align: center; +} +.justify-start{ + justify-content: start; +} +.justify-center{ + justify-content: center; +} +.justify-right{ + margin-left: auto; +} +.align-self-center{ + align-self: center; +} +.justify-self-center{ + justify-self: center; +} +.justify-self-start{ + justify-self: start; +} +.direction-column{ + flex-direction: column; +} +.space-between{ + justify-content: space-between; +} +.ripple{ + position: absolute; + border-radius: 50%; + transform: scale(0); + background: rgba(var(--text-color), 0.16); + pointer-events: none; +} +.interact{ + position: relative; + overflow: hidden; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} +.observe-empty-state:empty{ + display: none; +} +.observe-empty-state:not(:empty) ~ .empty-state{ + display: none; +} +.icon{ + width: 1.5rem; + height: 1.5rem; + fill: rgba(var(--text-color), 0.9); +} + +.page-layout{ + position: relative; + display: grid; + grid-template-columns: 1rem 1fr 1rem; + & > * { + grid-column: 2/3; + } +} +.popup__header{ + display: grid; + gap: 0.5rem; + width: 100%; + padding: 1rem 1.5rem; + align-items: center; + grid-template-columns: auto 1fr; +} +.popup__header__close{ + padding: 0.5rem; + cursor: pointer; +} + + +#loading_page, +#error_page{ + position: relative; + display: grid; + height: 100%; + place-content: center; + justify-items: center; +} + +#main_header{ + position: relative; + display: flex; + padding: 1rem; + align-items: center; + justify-content: space-between; + grid-template-columns: repeat(3, 1fr); +} +#main_header__logo{ + height: 1.8rem; + width: 1.8rem; +} + +.theme-switcher{ + position: relative; + justify-self: flex-end; + width: 1.5rem; + height: 1.5rem; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + .icon{ + position: absolute; + transition: transform 0.6s; + } +} +.theme-switcher__checkbox{ + display: none; + &:checked ~ .moon-icon{ + transform: scale(0) rotate(90deg); + } + &:not(:checked) ~ .sun-icon{ + transform: scale(0) rotate(-90deg); + } +} + +#search_section{ + display: grid; + gap: 1rem; + padding: 3rem 0; + justify-content: flex-start; +} + +#torrent_container{ + padding: 1.5rem 0; + display: grid; + gap: 0.5rem; + padding-bottom: 4rem; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); +} +.torrent-card{ + display: flex; + flex-direction: column; + cursor: pointer; + padding: 1rem; + border-radius: 0.3rem; + background-color: rgba(var(--foreground-color), 1); + &__icon{ + grid-area: icon; + display: flex; + padding: 0.6rem; + border-radius: 50%; + margin-bottom: 1.5rem; + align-items: center; + justify-content: center; + align-self: flex-start; + aspect-ratio: 1/1; + flex-shrink: 0; + background-color: var(--accent-color); + .icon{ + height: 1.6rem; + width: 1.6rem; + fill: white; + } + } + &__title{ + grid-area: title; + font-size: 1.2rem; + margin-bottom: 0.5rem; + } + &__tags{ + grid-area: tags; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 0.85rem; + margin-bottom: 1.5rem; + color: rgba(var(--text-color), 0.7); + } + &__description{ + font-size: 0.9rem; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + color: rgba(var(--text-color), 0.8); + } + &__size{ + font-weight: 500; + } + &__download-button{ + grid-area: button; + justify-self: flex-end; + margin-top: auto; + justify-content: center; + background-color: rgba(var(--text-color), 0.1); + .icon{ + margin-right: 0.5rem; + } + } +} +#torrent_popup{ + --width: min(42rem, 100%); +} +#main_footer{ + padding: 3rem 0; + background-color: rgba(var(--text-color), 0.06); +} +@media only screen and (max-width: 640px) { + .torrent-card{ + display: grid; + gap: 0 1rem; + align-items: center; + grid-template-columns: auto 1fr; + grid-template-areas: 'icon title' 'icon tags' 'icon button'; + &__icon{ + margin: 0; + } + &__tags{ + margin-bottom: 0; + } + &__description{ + display: none; + } + &__download-button{ + margin-top: 1rem; + } + } +} +@media only screen and (min-width: 640px) { + .page-layout{ + grid-template-columns: 1fr 90vw 1fr; + } + #main_header{ + padding: 1.5rem; + } + .torrent-card{ + padding: 1.5rem; + min-height: 22rem; + } +} +@media only screen and (min-width: 1280px) { + .page-layout{ + grid-template-columns: 1fr 90vw 1fr; + } + .multi-form{ + grid-template-columns: 1fr 1fr; + } +} +@media (any-hover: hover){ + ::-webkit-scrollbar{ + width: 0.5rem; + height: 0.5rem; + } + + ::-webkit-scrollbar-thumb{ + background: rgba(var(--text-color), 0.3); + border-radius: 1rem; + &:hover{ + background: rgba(var(--text-color), 0.5); + } + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..abf6fc1 --- /dev/null +++ b/index.html @@ -0,0 +1,793 @@ + + + + + + + + FLO Torrent + + + + + + + + + +

+
+ Cancel + OK +
+
+ + +

+

+
+
+
+ +
RanchiMall
+
+ +
+
+
+

FLO Torrent

+ + + +
+
+

Recent

+ +
+
+ + + + + + + + + + + \ No newline at end of file