luminomorphism
Version:
A UI design system built around light, blur, ambient motion and perceptual feedback.
274 lines (245 loc) • 14.6 kB
JavaScript
class LQuantumToggle extends HTMLElement{static get observedAttributes(){return["checked","disabled","size","color-on","color-off","quantum-mode","transition-speed","particle-count","superposition-enabled","label","glow-intensity"]}constructor(){super(),this.attachShadow({mode:"open"}),this.config={checked:!1,disabled:!1,size:"medium",colorOn:"#00ff80",colorOff:"#ff4444",quantumMode:!0,transitionSpeed:1,particleCount:8,superpositionEnabled:!0,label:"",glowIntensity:1},this.state={isTransitioning:!1,superpositionPhase:0,particles:[],quantumField:0,waveFunctions:[]},this.elements={},this.animationId=null,this.transitionTimeout=null,this.cleanup=[]}connectedCallback(){this.parseAttributes(),this.render(),this.initializeElements(),this.attachEventListeners(),this.initializeQuantumField(),this.startAnimation()}disconnectedCallback(){this.stopAnimation(),this.cleanup.forEach(t=>t())}attributeChangedCallback(t,e,s){e!==s&&(this.parseAttributes(),t==="checked"?this.updateToggleState():this.updateStyling())}parseAttributes(){this.config.checked=this.hasAttribute("checked"),this.config.disabled=this.hasAttribute("disabled"),this.config.size=this.getAttribute("size")||"medium",this.config.colorOn=this.getAttribute("color-on")||"#00ff80",this.config.colorOff=this.getAttribute("color-off")||"#ff4444",this.config.quantumMode=this.getAttribute("quantum-mode")!=="false",this.config.transitionSpeed=parseFloat(this.getAttribute("transition-speed"))||1,this.config.particleCount=parseInt(this.getAttribute("particle-count"))||8,this.config.superpositionEnabled=this.getAttribute("superposition-enabled")!=="false",this.config.label=this.getAttribute("label")||"",this.config.glowIntensity=parseFloat(this.getAttribute("glow-intensity"))||1}render(){const{size:t,colorOn:e,colorOff:s,label:i,disabled:n}=this.config,o={small:{width:"40px",height:"20px",thumb:"16px"},medium:{width:"60px",height:"30px",thumb:"24px"},large:{width:"80px",height:"40px",thumb:"32px"}},a=o[t]||o.medium;this.shadowRoot.innerHTML=`
<style>
:host {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: ${n?"not-allowed":"pointer"};
opacity: ${n?"0.6":"1"};
user-select: none;
}
.toggle-label {
font-size: 0.9rem;
color: #ffffff;
opacity: 0.9;
font-weight: 500;
}
.toggle-container {
position: relative;
width: ${a.width};
height: ${a.height};
cursor: ${n?"not-allowed":"pointer"};
}
.toggle-track {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
rgba(255,68,68,0.3) 0%,
rgba(0,255,128,0.3) 100%);
border: 2px solid rgba(255,255,255,0.2);
border-radius: ${parseInt(a.height)/2}px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.toggle-track.on {
background: linear-gradient(90deg,
${e}40 0%,
${e}20 100%);
border-color: ${e}60;
box-shadow:
0 0 20px ${e}30,
inset 0 2px 4px rgba(0,0,0,0.2);
}
.toggle-track.off {
background: linear-gradient(90deg,
${s}40 0%,
${s}20 100%);
border-color: ${s}60;
box-shadow:
0 0 20px ${s}30,
inset 0 2px 4px rgba(0,0,0,0.2);
}
.quantum-field {
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: radial-gradient(circle, transparent 40%, rgba(255,255,255,0.05) 100%);
border-radius: ${parseInt(a.height)/2+10}px;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.quantum-field.active {
opacity: 1;
animation: quantumPulse 2s ease-in-out infinite;
}
.toggle-thumb {
position: absolute;
top: 50%;
left: 3px;
width: ${a.thumb};
height: ${a.thumb};
background: linear-gradient(135deg, #ffffff, #f0f0f0);
border: 2px solid rgba(0,0,0,0.1);
border-radius: 50%;
transform: translateY(-50%);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow:
0 4px 12px rgba(0,0,0,0.15),
0 2px 4px rgba(0,0,0,0.1);
z-index: 10;
}
.toggle-thumb.on {
left: calc(100% - ${a.thumb} - 3px);
background: linear-gradient(135deg, ${e}, ${e}cc);
border-color: ${e};
box-shadow:
0 4px 16px rgba(0,0,0,0.2),
0 0 24px ${e}60,
inset 0 1px 2px rgba(255,255,255,0.3);
}
.toggle-thumb.off {
left: 3px;
background: linear-gradient(135deg, ${s}, ${s}cc);
border-color: ${s};
box-shadow:
0 4px 16px rgba(0,0,0,0.2),
0 0 24px ${s}60,
inset 0 1px 2px rgba(255,255,255,0.3);
}
.toggle-thumb.superposition {
animation: superpositionFlicker 0.8s ease-in-out;
background: linear-gradient(135deg,
${s} 0%,
#ffffff 30%,
${e} 60%,
#ffffff 100%);
box-shadow:
0 4px 20px rgba(0,0,0,0.25),
0 0 30px rgba(255,255,255,0.6),
0 0 40px ${e}40,
0 0 40px ${s}40;
}
.quantum-particle {
position: absolute;
width: 3px;
height: 3px;
background: #ffffff;
border-radius: 50%;
pointer-events: none;
opacity: 0;
z-index: 5;
}
.wave-function {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255,255,255,0.6) 50%,
transparent 100%);
transform: translateY(-50%);
opacity: 0;
pointer-events: none;
z-index: 3;
}
.wave-function.active {
opacity: 1;
animation: waveMotion 1s ease-in-out;
}
.transition-effect {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle,
rgba(255,255,255,0.8) 0%,
transparent 70%);
border-radius: ${parseInt(a.height)/2}px;
opacity: 0;
pointer-events: none;
z-index: 8;
}
.transition-effect.active {
opacity: 1;
animation: transitionFlash 0.6s ease-out;
}
quantumPulse {
0%, 100% {
transform: scale(1) rotate(0deg);
opacity: 0.3;
}
50% {
transform: scale(1.05) rotate(180deg);
opacity: 0.6;
}
}
superpositionFlicker {
0% { background-position: 0% 50%; }
25% { background-position: 100% 50%; }
50% { background-position: 50% 0%; }
75% { background-position: 50% 100%; }
100% { background-position: 0% 50%; }
}
waveMotion {
0% {
transform: translateY(-50%) scaleX(0);
opacity: 0;
}
50% {
transform: translateY(-50%) scaleX(1);
opacity: 1;
}
100% {
transform: translateY(-50%) scaleX(0);
opacity: 0;
}
}
transitionFlash {
0% { opacity: 0; transform: scale(0.5); }
50% { opacity: 1; transform: scale(1.2); }
100% { opacity: 0; transform: scale(2); }
}
particleFloat {
0% {
opacity: 0;
transform: translate(0, 0) scale(0.5);
}
50% {
opacity: 1;
transform: translate(var(--dx), var(--dy)) scale(1);
}
100% {
opacity: 0;
transform: translate(var(--dx2), var(--dy2)) scale(0.3);
}
}
/* Hover effects */
.toggle-container:hover .toggle-thumb {
transform: translateY(-50%) scale(1.05);
}
.toggle-container:hover .quantum-field {
opacity: 0.8;
}
/* Focus styles */
:host(:focus-within) .toggle-track {
outline: 2px solid rgba(255,255,255,0.5);
outline-offset: 2px;
}
/* Disabled state */
:host([disabled]) .toggle-track {
filter: grayscale(1);
opacity: 0.6;
}
:host([disabled]) .toggle-thumb {
filter: grayscale(1);
}
</style>
<div class="toggle-container" id="toggleContainer">
<div class="quantum-field" id="quantumField"></div>
<div class="toggle-track ${this.config.checked?"on":"off"}" id="toggleTrack">
<div class="wave-function" id="waveFunction"></div>
<div class="transition-effect" id="transitionEffect"></div>
</div>
<div class="toggle-thumb ${this.config.checked?"on":"off"}" id="toggleThumb"></div>
</div>
${i?`<span class="toggle-label">${i}</span>`:""}
`}initializeElements(){this.elements={container:this.shadowRoot.getElementById("toggleContainer"),track:this.shadowRoot.getElementById("toggleTrack"),thumb:this.shadowRoot.getElementById("toggleThumb"),quantumField:this.shadowRoot.getElementById("quantumField"),waveFunction:this.shadowRoot.getElementById("waveFunction"),transitionEffect:this.shadowRoot.getElementById("transitionEffect")},this.setAttribute("tabindex","0")}attachEventListeners(){const t=n=>this.handleClick(n),e=n=>this.handleKeydown(n),s=n=>this.handleFocus(n),i=n=>this.handleBlur(n);this.addEventListener("click",t),this.addEventListener("keydown",e),this.addEventListener("focus",s),this.addEventListener("blur",i),this.cleanup.push(()=>{this.removeEventListener("click",t),this.removeEventListener("keydown",e),this.removeEventListener("focus",s),this.removeEventListener("blur",i)})}handleClick(t){this.config.disabled||(t.preventDefault(),this.toggle())}handleKeydown(t){this.config.disabled||(t.key===" "||t.key==="Enter")&&(t.preventDefault(),this.toggle())}handleFocus(t){this.config.quantumMode&&this.elements.quantumField.classList.add("active")}handleBlur(t){this.elements.quantumField.classList.remove("active")}toggle(){this.state.isTransitioning||(this.config.checked=!this.config.checked,this.config.checked?this.setAttribute("checked",""):this.removeAttribute("checked"),this.performQuantumTransition(),this.dispatchEvent(new CustomEvent("change",{detail:{checked:this.config.checked},bubbles:!0})))}performQuantumTransition(){if(!this.config.quantumMode){this.updateToggleState();return}this.state.isTransitioning=!0,this.config.superpositionEnabled&&this.enterSuperposition(),this.triggerWaveCollapse(),this.createQuantumParticles(),this.elements.transitionEffect.classList.add("active"),setTimeout(()=>{this.elements.transitionEffect.classList.remove("active")},600);const t=800/this.config.transitionSpeed;this.transitionTimeout=setTimeout(()=>{this.completeTransition()},t)}enterSuperposition(){this.elements.thumb.classList.add("superposition"),this.elements.quantumField.classList.add("active"),setTimeout(()=>{this.elements.thumb.classList.remove("superposition"),this.elements.quantumField.classList.remove("active")},800/this.config.transitionSpeed)}triggerWaveCollapse(){this.elements.waveFunction.classList.add("active"),setTimeout(()=>{this.elements.waveFunction.classList.remove("active")},1e3/this.config.transitionSpeed)}createQuantumParticles(){const t=this.elements.container,e=this.config.particleCount;for(let s=0;s<e;s++){const i=document.createElement("div");i.className="quantum-particle";const n=Math.random()*t.offsetWidth,o=Math.random()*t.offsetHeight,a=(Math.random()-.5)*40,r=(Math.random()-.5)*40,l=a*2,c=r*2;i.style.left=`${n}px`,i.style.top=`${o}px`,i.style.setProperty("--dx",`${a}px`),i.style.setProperty("--dy",`${r}px`),i.style.setProperty("--dx2",`${l}px`),i.style.setProperty("--dy2",`${c}px`),i.style.background=this.config.checked?this.config.colorOn:this.config.colorOff,i.style.animation=`particleFloat ${1.5/this.config.transitionSpeed}s ease-out`,t.appendChild(i),setTimeout(()=>{i.parentNode&&i.parentNode.removeChild(i)},1.5*1e3/this.config.transitionSpeed)}}completeTransition(){this.updateToggleState(),this.state.isTransitioning=!1}updateToggleState(){const{thumb:t,track:e}=this.elements;this.config.checked?(t.classList.remove("off"),t.classList.add("on"),e.classList.remove("off"),e.classList.add("on")):(t.classList.remove("on"),t.classList.add("off"),e.classList.remove("on"),e.classList.add("off"))}updateStyling(){this.render(),this.initializeElements(),this.attachEventListeners(),this.updateToggleState()}initializeQuantumField(){this.config.quantumMode&&(this.state.waveFunctions=Array.from({length:3},(t,e)=>({amplitude:Math.random()*.5+.3,frequency:(e+1)*.1,phase:Math.random()*Math.PI*2})))}startAnimation(){if(!this.config.quantumMode)return;const t=()=>{if(this.state.quantumField+=.02,this.elements.quantumField.classList.contains("active")){const e=Math.sin(this.state.quantumField)*.3+.7;this.elements.quantumField.style.opacity=e*this.config.glowIntensity}this.animationId=requestAnimationFrame(t)};t()}stopAnimation(){this.animationId&&(cancelAnimationFrame(this.animationId),this.animationId=null),this.transitionTimeout&&(clearTimeout(this.transitionTimeout),this.transitionTimeout=null)}check(){this.config.checked||this.toggle()}uncheck(){this.config.checked&&this.toggle()}isChecked(){return this.config.checked}enable(){this.config.disabled=!1,this.removeAttribute("disabled"),this.style.opacity="1",this.style.cursor="pointer"}disable(){this.config.disabled=!0,this.setAttribute("disabled",""),this.style.opacity="0.6",this.style.cursor="not-allowed"}}customElements.define("l-quantum-toggle",LQuantumToggle);