Explorar o código

简单的animal

robert hai 4 días
pai
achega
2980f4b9ad
Modificáronse 7 ficheiros con 1015 adicións e 0 borrados
  1. 6 0
      README.md
  2. 378 0
      animal.js
  3. 109 0
      doc/animal.html
  4. 369 0
      layer.js
  5. 19 0
      main.go
  6. 134 0
      test.html
  7. 0 0
      xjs.js

+ 6 - 0
README.md

@@ -1,2 +1,8 @@
 # xjs
 
+这个库用来最小化的集成一个可用的js库,用于引用在一个网站中的组件,兼容app、自适应
+
+
+# 弹出层
+
+

+ 378 - 0
animal.js

@@ -0,0 +1,378 @@
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+    typeof define === 'function' && define.amd ? define(factory) :
+      (global.animal = factory());
+}(this, (function () {
+  'use strict';
+
+  // --- Utils ---
+  const isArr = (a) => Array.isArray(a);
+  const isStr = (s) => typeof s === 'string';
+  const isFunc = (f) => typeof f === 'function';
+  const isNil = (v) => v === undefined || v === null;
+  const isSVG = (el) => el instanceof SVGElement;
+  
+  const toArray = (targets) => {
+    if (isArr(targets)) return targets;
+    if (isStr(targets)) return Array.from(document.querySelectorAll(targets));
+    if (targets instanceof NodeList) return Array.from(targets);
+    if (targets instanceof Element || targets instanceof Window) return [targets];
+    return [];
+  };
+
+  const UNITLESS_KEYS = ['opacity', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'zIndex', 'fontWeight', 'strokeDashoffset', 'strokeDasharray', 'strokeWidth'];
+
+  const getUnit = (val, prop) => {
+    if (UNITLESS_KEYS.includes(prop)) return '';
+    const split = /[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(val);
+    return split ? split[1] : undefined;
+  };
+
+  // --- Spring Physics (Simplified) ---
+  // Returns an array of [time, value] or just value for WAAPI linear easing
+  function spring({ stiffness = 100, damping = 10, mass = 1, velocity = 0, precision = 0.01 } = {}) {
+    const values = [];
+    let t = 0;
+    const timeStep = 1 / 60; // 60fps simulation
+    let current = 0;
+    let v = velocity;
+    const target = 1;
+
+    let running = true;
+    while (running && t < 10) { // Safety break at 10s
+      const fSpring = -stiffness * (current - target);
+      const fDamper = -damping * v;
+      const a = (fSpring + fDamper) / mass;
+      
+      v += a * timeStep;
+      current += v * timeStep;
+      
+      values.push(current);
+      t += timeStep;
+
+      if (Math.abs(current - target) < precision && Math.abs(v) < precision) {
+        running = false;
+      }
+    }
+    if (values[values.length - 1] !== 1) values.push(1);
+    
+    return values;
+  }
+
+  // --- WAAPI Core ---
+  const TRANSFORMS = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'perspective', 'x', 'y'];
+  
+  const ALIASES = {
+    x: 'translateX',
+    y: 'translateY',
+    z: 'translateZ'
+  };
+
+  // Basic rAF loop for non-WAAPI props or fallback
+  class RafEngine {
+    constructor() {
+      this.animations = [];
+      this.tick = this.tick.bind(this);
+      this.running = false;
+    }
+    add(anim) {
+      this.animations.push(anim);
+      if (!this.running) {
+        this.running = true;
+        requestAnimationFrame(this.tick);
+      }
+    }
+    tick(t) {
+      const now = t;
+      this.animations = this.animations.filter(anim => {
+        return anim.tick(now); // return true to keep
+      });
+      if (this.animations.length) {
+        requestAnimationFrame(this.tick);
+      } else {
+        this.running = false;
+      }
+    }
+  }
+  const rafEngine = new RafEngine();
+
+  // --- Main Animation Logic ---
+  function animate(targets, params) {
+    const elements = toArray(targets);
+    const { 
+      duration = 1000, 
+      delay = 0, 
+      easing = 'ease-out', 
+      direction = 'normal', 
+      fill = 'forwards',
+      loop = 1,
+      endDelay = 0,
+      autoplay = true,
+      update, // callback
+      begin, // callback
+      complete // callback
+    } = params;
+
+    const animatableProps = {};
+    const waapiProps = {};
+    const jsProps = {};
+    let isSpring = false;
+    let springValues = null;
+
+    if (typeof easing === 'object' && !Array.isArray(easing)) {
+        isSpring = true;
+        springValues = spring(easing);
+    }
+
+    Object.keys(params).forEach(key => {
+      if (['duration', 'delay', 'easing', 'direction', 'fill', 'loop', 'endDelay', 'autoplay', 'update', 'begin', 'complete'].includes(key)) return;
+      
+      let prop = ALIASES[key] || key;
+      const isTransform = TRANSFORMS.includes(prop) || TRANSFORMS.includes(key);
+      const isCss = !isTransform && (key === 'opacity' || key === 'filter'); // Simplified check
+      
+      const val = params[key];
+      
+      if (isTransform || isCss) {
+        waapiProps[key] = val;
+      } else {
+        jsProps[key] = val;
+      }
+    });
+
+    const promises = elements.map(el => {
+      // 1. WAAPI Animation
+      let waapiAnim = null;
+      let waapiPromise = Promise.resolve();
+      
+      if (Object.keys(waapiProps).length > 0) {
+        
+        const generateFrames = (propValues) => {
+            if (isSpring && springValues) {
+                return springValues.map(v => {
+                    const frame = {};
+                    Object.keys(propValues).forEach(k => {
+                        const targetVal = parseFloat(propValues[k]);
+                        const unit = getUnit(propValues[k], k) || (k.startsWith('rotate') ? 'deg' : (UNITLESS_KEYS.includes(k) ? '' : 'px'));
+                        
+                        let startVal = 0;
+                        if (k.startsWith('scale')) startVal = 1;
+                        if (k === 'opacity') startVal = parseFloat(getComputedStyle(el).opacity);
+                        
+                        if (Array.isArray(propValues[k])) {
+                             startVal = parseFloat(propValues[k][0]);
+                             const endVal = parseFloat(propValues[k][1]);
+                             const current = startVal + (endVal - startVal) * v;
+                             frame[k] = current + unit;
+                        } else {
+                             const endVal = targetVal;
+                             const current = startVal + (endVal - startVal) * v;
+                             frame[k] = current + unit;
+                        }
+                    });
+                    return frame;
+                });
+            } else {
+                const frame0 = {};
+                const frame1 = {};
+                
+                Object.keys(propValues).forEach(k => {
+                   let val = propValues[k];
+                   if (Array.isArray(val)) {
+                       frame0[k] = val[0];
+                       frame1[k] = val[1];
+                   } else {
+                       frame1[k] = val;
+                   }
+                });
+                return [frame0, frame1]; 
+            }
+        };
+
+        let rawFrames = generateFrames(waapiProps);
+        
+        const finalFrames = rawFrames.map(f => {
+            const final = {};
+            let transformStr = '';
+            Object.keys(f).forEach(k => {
+                const isT = TRANSFORMS.includes(ALIASES[k] || k);
+                if (isT) {
+                    let key = ALIASES[k] || k;
+                    if (key === 'x') key = 'translateX';
+                    if (key === 'y') key = 'translateY';
+                    if (key === 'z') key = 'translateZ';
+                    
+                    let val = f[k];
+                    if (typeof val === 'number') {
+                        if (key.startsWith('scale')) {
+                            val = val + '';
+                        } else if (key.startsWith('rotate') || key.startsWith('skew')) {
+                            val = val + 'deg';
+                        } else {
+                            val = val + 'px';
+                        }
+                    }
+                    transformStr += `${key}(${val}) `;
+                } else {
+                    final[k] = f[k];
+                }
+            });
+            if (transformStr) final.transform = transformStr.trim();
+            return final;
+        });
+
+        if (Object.keys(finalFrames[0]).length === 0) finalFrames.shift();
+
+        const opts = {
+            duration: isSpring ? (springValues.length * 1000 / 60) : duration,
+            delay,
+            fill,
+            iterations: loop,
+            easing: isSpring ? 'linear' : easing 
+        };
+
+        const animation = el.animate(finalFrames, opts);
+        waapiAnim = animation;
+        waapiPromise = animation.finished;
+      }
+
+      // 2. JS Animation (Fallback / Attributes)
+      let jsPromise = Promise.resolve();
+      if (Object.keys(jsProps).length > 0) {
+          jsPromise = new Promise(resolve => {
+             const startTime = performance.now() + delay;
+             const initialValues = {};
+             
+             // Setup
+             Object.keys(jsProps).forEach(k => {
+                 if (k === 'scrollTop' || k === 'scrollLeft') {
+                     initialValues[k] = el[k];
+                 } else if (k in el.style) {
+                     // For style props not in WAAPI whitelist (like strokeDashoffset)
+                     initialValues[k] = parseFloat(el.style[k]) || 0; // simplistic
+                 } else if (isSVG(el) && el.hasAttribute(k)) {
+                     initialValues[k] = parseFloat(el.getAttribute(k)) || 0;
+                 } else {
+                     initialValues[k] = 0; 
+                 }
+             });
+
+             const tick = (now) => {
+                 if (now < startTime) return true;
+                 const elapsed = now - startTime;
+                 let progress = Math.min(elapsed / duration, 1);
+                 
+                 Object.keys(jsProps).forEach(k => {
+                    const target = jsProps[k];
+                    let start = initialValues[k];
+                    let end = isArr(target) ? target[1] : target;
+                    if (isArr(target)) start = target[0];
+                    
+                    const val = start + (end - start) * progress;
+                    
+                    if (k === 'scrollTop' || k === 'scrollLeft') {
+                        el[k] = val;
+                    } else if (k in el.style) {
+                        el.style[k] = val; // Set as style
+                    } else if (isSVG(el)) {
+                        // Kebab-case conversion for setAttribute
+                        const attr = k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
+                        el.setAttribute(attr, val);
+                    } else {
+                        el[k] = val; 
+                    }
+                    if (update) update({ target: el, progress, value: val });
+                 });
+
+                 if (progress < 1) return true;
+                 if (complete) complete(el);
+                 resolve();
+                 return false;
+             };
+             
+             rafEngine.add({ tick });
+          });
+      }
+
+      return Promise.all([waapiPromise, jsPromise]);
+    });
+
+    const masterPromise = Promise.all(promises);
+    masterPromise.then = (fn) => Promise.all(promises).then(fn);
+    return masterPromise;
+  }
+
+  // --- SVG Draw ---
+  function svgDraw(targets, params = {}) {
+     const elements = toArray(targets);
+     elements.forEach(el => {
+         if (!isSVG(el)) return;
+         const len = el.getTotalLength ? el.getTotalLength() : 0;
+         el.style.strokeDasharray = len;
+         el.style.strokeDashoffset = len; 
+         
+         // Animate offset to 0
+         animate(el, {
+             strokeDashoffset: [len, 0],
+             ...params
+         });
+     });
+  }
+
+  // --- In View ---
+  function inViewAnimate(targets, params, options = {}) {
+      const elements = toArray(targets);
+      const observer = new IntersectionObserver((entries) => {
+          entries.forEach(entry => {
+              if (entry.isIntersecting) {
+                  animate(entry.target, params);
+                  if (options.once !== false) observer.unobserve(entry.target);
+              }
+          });
+      }, { threshold: options.threshold || 0.1 });
+      
+      elements.forEach(el => observer.observe(el));
+  }
+
+  // --- Timeline ---
+  function timeline(defaults = {}) {
+      const realTimeApi = {
+          currentTime: 0,
+          add: (targets, params, offset) => {
+               const animParams = { ...defaults, ...params };
+               let start = realTimeApi.currentTime;
+               
+               if (offset !== undefined) {
+                  if (isStr(offset) && offset.startsWith('-=')) start -= parseFloat(offset.slice(2));
+                  else if (isStr(offset) && offset.startsWith('+=')) start += parseFloat(offset.slice(2));
+                  else if (typeof offset === 'number') start = offset; 
+               }
+               
+               // Use setTimeout to defer EXECUTION until the exact start time
+               // This ensures that "implicit from" values are read correctly from the DOM
+               // at the moment the animation is supposed to start.
+               if (start <= 0) {
+                   animate(targets, animParams);
+               } else {
+                   setTimeout(() => {
+                       animate(targets, animParams);
+                   }, start);
+               }
+               
+               const dur = animParams.duration || 1000;
+               realTimeApi.currentTime = Math.max(realTimeApi.currentTime, start + dur);
+               return realTimeApi;
+          }
+      };
+      return realTimeApi;
+  }
+
+  return {
+    animate,
+    timeline,
+    svgDraw,
+    inViewAnimate,
+    spring
+  };
+
+})));

+ 109 - 0
doc/animal.html

@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Animal.js Demo</title>
+    <style>
+        body { font-family: sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; padding-bottom: 500px; }
+        section { margin-bottom: 50px; border-bottom: 1px solid #eee; padding-bottom: 20px; }
+        .box { width: 50px; height: 50px; background: #3498db; border-radius: 4px; margin-bottom: 10px; }
+        .circle { width: 50px; height: 50px; background: #e74c3c; border-radius: 50%; }
+        .btn { padding: 8px 16px; cursor: pointer; background: #333; color: white; border: none; border-radius: 4px; margin-top: 10px; }
+        svg { border: 1px solid #ddd; }
+    </style>
+</head>
+<body>
+    <h1>Animal.js Demo</h1>
+
+    <section>
+        <h2>1. Basic Transform & Opacity (WAAPI)</h2>
+        <div class="box box-1"></div>
+        <button class="btn" onclick="runBasic()">Animate</button>
+        <pre>animal.animate('.box-1', { x: 200, rotate: 180, opacity: 0.5, duration: 1000 })</pre>
+    </section>
+
+    <section>
+        <h2>2. Spring Physics</h2>
+        <div class="box box-2"></div>
+        <button class="btn" onclick="runSpring()">Spring</button>
+        <pre>animal.animate('.box-2', { x: 200, easing: { stiffness: 200, damping: 10 } })</pre>
+    </section>
+
+    <section>
+        <h2>3. Timeline Sequence</h2>
+        <div class="box box-3a" style="background: purple"></div>
+        <div class="box box-3b" style="background: orange"></div>
+        <button class="btn" onclick="runTimeline()">Run Timeline</button>
+    </section>
+
+    <section>
+        <h2>4. SVG Draw</h2>
+        <svg width="200" height="100" viewBox="0 0 200 100">
+            <path class="path-1" fill="none" stroke="#2ecc71" stroke-width="5" d="M10,50 Q50,5 90,50 T190,50" />
+        </svg>
+        <br>
+        <button class="btn" onclick="runSvg()">Draw SVG</button>
+    </section>
+
+    <section>
+        <h2>5. In View Trigger</h2>
+        <p>Scroll down to see the animation...</p>
+        <div style="height: 110vh; background: linear-gradient(to bottom, #f9f9f9, #eee); display: flex; align-items: center; justify-content: center; color: #999;">
+            Scroll Spacer (Keep scrolling)
+        </div>
+        <div class="box box-view" style="opacity: 0;"></div>
+    </section>
+
+    <script src="../animal.js"></script>
+    <script>
+        // 1. Basic
+        function runBasic() {
+            animal.animate('.box-1', { 
+                x: 200, 
+                rotate: 180, 
+                opacity: 0.5, 
+                duration: 1000,
+                easing: 'ease-in-out'
+            }).then(() => {
+                console.log('Basic finished');
+                // Chain back
+                animal.animate('.box-1', { x: 0, rotate: 0, opacity: 1, duration: 500 });
+            });
+        }
+
+        // 2. Spring
+        function runSpring() {
+            animal.animate('.box-2', { 
+                x: 200, 
+                easing: { stiffness: 150, damping: 8 } 
+            });
+        }
+
+        // 3. Timeline
+        function runTimeline() {
+            const tl = animal.timeline();
+            tl.add('.box-3a', { x: 100, duration: 500 })
+              .add('.box-3b', { x: 100, rotate: 90, duration: 800 }, "-=200") // Overlap
+              .add('.box-3a', { x: 0, duration: 500 })
+              .add('.box-3b', { x: 0, rotate: 0, duration: 500 });
+        }
+
+        // 4. SVG
+        function runSvg() {
+            animal.svgDraw('.path-1', { duration: 1500, easing: 'ease-in-out' });
+        }
+
+        // 5. In View
+        // Use explicit from-to for spring to ensure correct start position
+        animal.inViewAnimate('.box-view', {
+            x: [-100, 0], 
+            opacity: [0, 1],
+            duration: 1000,
+            easing: { stiffness: 50, damping: 15 } // slow spring
+        }, { threshold: 0.5 });
+
+    </script>
+</body>
+</html>
+

+ 369 - 0
layer.js

@@ -0,0 +1,369 @@
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+    typeof define === 'function' && define.amd ? define(factory) :
+      (global.Layer = factory());
+}(this, (function () {
+  'use strict';
+
+  const PREFIX = 'layer-';
+  
+  // Theme & Styles
+  const css = `
+    .${PREFIX}overlay {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: rgba(0, 0, 0, 0.4);
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      z-index: 1000;
+      opacity: 0;
+      transition: opacity 0.3s;
+    }
+    .${PREFIX}overlay.show {
+      opacity: 1;
+    }
+    .${PREFIX}popup {
+      background: #fff;
+      border-radius: 8px;
+      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+      width: 32em;
+      max-width: 90%;
+      padding: 1.5em;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      transform: scale(0.9);
+      transition: transform 0.3s;
+      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+    }
+    .${PREFIX}overlay.show .${PREFIX}popup {
+      transform: scale(1);
+    }
+    .${PREFIX}title {
+      font-size: 1.8em;
+      font-weight: 600;
+      color: #333;
+      margin: 0 0 0.5em;
+      text-align: center;
+    }
+    .${PREFIX}content {
+      font-size: 1.125em;
+      color: #545454;
+      margin-bottom: 1.5em;
+      text-align: center;
+      line-height: 1.5;
+    }
+    .${PREFIX}actions {
+      display: flex;
+      justify-content: center;
+      gap: 1em;
+      width: 100%;
+    }
+    .${PREFIX}button {
+      border: none;
+      border-radius: 4px;
+      padding: 0.6em 1.2em;
+      font-size: 1em;
+      cursor: pointer;
+      transition: background-color 0.2s, box-shadow 0.2s;
+      color: #fff;
+    }
+    .${PREFIX}confirm {
+      background-color: #3085d6;
+    }
+    .${PREFIX}confirm:hover {
+      background-color: #2b77c0;
+    }
+    .${PREFIX}cancel {
+      background-color: #aaa;
+    }
+    .${PREFIX}cancel:hover {
+      background-color: #999;
+    }
+    .${PREFIX}icon {
+      width: 5em;
+      height: 5em;
+      border: 0.25em solid transparent;
+      border-radius: 50%;
+      margin: 1.5em auto 1.2em;
+      box-sizing: content-box;
+      position: relative;
+    }
+    
+    /* Success Icon */
+    .${PREFIX}icon.success {
+      border-color: #a5dc86;
+      color: #a5dc86;
+    }
+    .${PREFIX}icon.success::before, .${PREFIX}icon.success::after {
+      content: '';
+      position: absolute;
+      background: #fff;
+      border-radius: 50%;
+    }
+    .${PREFIX}success-line-tip {
+      width: 1.5em;
+      height: 0.3em;
+      background-color: #a5dc86;
+      display: block;
+      position: absolute;
+      top: 2.85em;
+      left: 0.85em;
+      transform: rotate(45deg);
+      border-radius: 0.15em;
+    }
+    .${PREFIX}success-line-long {
+      width: 2.9em;
+      height: 0.3em;
+      background-color: #a5dc86;
+      display: block;
+      position: absolute;
+      top: 2.4em;
+      right: 0.5em;
+      transform: rotate(-45deg);
+      border-radius: 0.15em;
+    }
+
+    /* Error Icon */
+    .${PREFIX}icon.error {
+      border-color: #f27474;
+      color: #f27474;
+    }
+    .${PREFIX}error-x-mark {
+      position: relative;
+      display: block;
+    }
+    .${PREFIX}error-line {
+      position: absolute;
+      height: 0.3em;
+      width: 3em;
+      background-color: #f27474;
+      display: block;
+      top: 2.3em;
+      left: 1em;
+      border-radius: 0.15em;
+    }
+    .${PREFIX}error-line.left {
+      transform: rotate(45deg);
+    }
+    .${PREFIX}error-line.right {
+      transform: rotate(-45deg);
+    }
+    
+    /* Warning Icon */
+    .${PREFIX}icon.warning {
+      border-color: #facea8;
+      color: #f8bb86;
+    }
+    .${PREFIX}warning-body {
+      position: absolute;
+      width: 0.3em;
+      height: 2.9em;
+      background-color: #f8bb86;
+      left: 50%;
+      top: 0.6em;
+      margin-left: -0.15em;
+      border-radius: 0.15em;
+    }
+    .${PREFIX}warning-dot {
+      position: absolute;
+      width: 0.3em;
+      height: 0.3em;
+      background-color: #f8bb86;
+      left: 50%;
+      bottom: 0.6em;
+      margin-left: -0.15em;
+      border-radius: 50%;
+    }
+  `;
+
+  // Inject Styles
+  const injectStyles = () => {
+    if (document.getElementById(`${PREFIX}styles`)) return;
+    const styleSheet = document.createElement('style');
+    styleSheet.id = `${PREFIX}styles`;
+    styleSheet.textContent = css;
+    document.head.appendChild(styleSheet);
+  };
+
+  class Layer {
+    constructor() {
+      injectStyles();
+      this.params = {};
+      this.dom = {};
+      this.promise = null;
+      this.resolve = null;
+      this.reject = null;
+    }
+
+    // Static entry point
+    static fire(options) {
+      const instance = new Layer();
+      return instance._fire(options);
+    }
+
+    _fire(options = {}) {
+      if (typeof options === 'string') {
+        options = { title: options };
+        if (arguments[1]) options.text = arguments[1];
+        if (arguments[2]) options.icon = arguments[2];
+      }
+
+      this.params = {
+        title: '',
+        text: '',
+        icon: null,
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        showCancelButton: false,
+        confirmButtonColor: '#3085d6',
+        cancelButtonColor: '#aaa',
+        closeOnClickOutside: true,
+        ...options
+      };
+
+      this.promise = new Promise((resolve, reject) => {
+        this.resolve = resolve;
+        this.reject = reject;
+      });
+
+      this._render();
+      return this.promise;
+    }
+
+    _render() {
+      // Remove existing if any
+      const existing = document.querySelector(`.${PREFIX}overlay`);
+      if (existing) existing.remove();
+
+      // Create Overlay
+      this.dom.overlay = document.createElement('div');
+      this.dom.overlay.className = `${PREFIX}overlay`;
+
+      // Create Popup
+      this.dom.popup = document.createElement('div');
+      this.dom.popup.className = `${PREFIX}popup`;
+      this.dom.overlay.appendChild(this.dom.popup);
+
+      // Icon
+      if (this.params.icon) {
+        this.dom.icon = this._createIcon(this.params.icon);
+        this.dom.popup.appendChild(this.dom.icon);
+      }
+
+      // Title
+      if (this.params.title) {
+        this.dom.title = document.createElement('h2');
+        this.dom.title.className = `${PREFIX}title`;
+        this.dom.title.textContent = this.params.title;
+        this.dom.popup.appendChild(this.dom.title);
+      }
+
+      // Text
+      if (this.params.text) {
+        this.dom.content = document.createElement('div');
+        this.dom.content.className = `${PREFIX}content`;
+        this.dom.content.textContent = this.params.text;
+        this.dom.popup.appendChild(this.dom.content);
+      }
+
+      // Actions
+      this.dom.actions = document.createElement('div');
+      this.dom.actions.className = `${PREFIX}actions`;
+      
+      // Cancel Button
+      if (this.params.showCancelButton) {
+        this.dom.cancelBtn = document.createElement('button');
+        this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
+        this.dom.cancelBtn.textContent = this.params.cancelButtonText;
+        this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
+        this.dom.cancelBtn.onclick = () => this._close(false);
+        this.dom.actions.appendChild(this.dom.cancelBtn);
+      }
+
+      // Confirm Button
+      this.dom.confirmBtn = document.createElement('button');
+      this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
+      this.dom.confirmBtn.textContent = this.params.confirmButtonText;
+      this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
+      this.dom.confirmBtn.onclick = () => this._close(true);
+      this.dom.actions.appendChild(this.dom.confirmBtn);
+
+      this.dom.popup.appendChild(this.dom.actions);
+
+      // Event Listeners
+      if (this.params.closeOnClickOutside) {
+        this.dom.overlay.addEventListener('click', (e) => {
+          if (e.target === this.dom.overlay) {
+            this._close(null); // Dismiss
+          }
+        });
+      }
+
+      document.body.appendChild(this.dom.overlay);
+
+      // Animation
+      requestAnimationFrame(() => {
+        this.dom.overlay.classList.add('show');
+      });
+    }
+
+    _createIcon(type) {
+      const icon = document.createElement('div');
+      icon.className = `${PREFIX}icon ${type}`;
+      
+      if (type === 'success') {
+        const tip = document.createElement('div');
+        tip.className = `${PREFIX}success-line-tip`;
+        const long = document.createElement('div');
+        long.className = `${PREFIX}success-line-long`;
+        icon.appendChild(tip);
+        icon.appendChild(long);
+      } else if (type === 'error') {
+        const xMark = document.createElement('span');
+        xMark.className = `${PREFIX}error-x-mark`;
+        const left = document.createElement('span');
+        left.className = `${PREFIX}error-line left`;
+        const right = document.createElement('span');
+        right.className = `${PREFIX}error-line right`;
+        xMark.appendChild(left);
+        xMark.appendChild(right);
+        icon.appendChild(xMark);
+      } else if (type === 'warning') {
+        const body = document.createElement('div');
+        body.className = `${PREFIX}warning-body`;
+        const dot = document.createElement('div');
+        dot.className = `${PREFIX}warning-dot`;
+        icon.appendChild(body);
+        icon.appendChild(dot);
+      }
+      
+      return icon;
+    }
+
+    _close(isConfirmed) {
+      this.dom.overlay.classList.remove('show');
+      setTimeout(() => {
+        if (this.dom.overlay && this.dom.overlay.parentNode) {
+          this.dom.overlay.parentNode.removeChild(this.dom.overlay);
+        }
+      }, 300);
+
+      if (isConfirmed === true) {
+        this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
+      } else if (isConfirmed === false) {
+        this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'cancel' });
+      } else {
+        this.resolve({ isConfirmed: false, isDenied: false, isDismissed: true, dismiss: 'backdrop' });
+      }
+    }
+  }
+
+  return Layer;
+
+})));
+

+ 19 - 0
main.go

@@ -0,0 +1,19 @@
+package main
+
+import (
+	"log"
+	"net/http"
+)
+
+func main() {
+	// Serve files from the current directory
+	fs := http.FileServer(http.Dir("."))
+	http.Handle("/", fs)
+
+	log.Println("Listening on :8080...")
+	err := http.ListenAndServe(":8080", nil)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+

+ 134 - 0
test.html

@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Layer.js Test</title>
+    <style>
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+            text-align: center;
+            padding: 50px;
+            background: #f0f2f5;
+        }
+        h1 { margin-bottom: 30px; }
+        .btn-group {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: center;
+            gap: 15px;
+        }
+        button {
+            padding: 10px 20px;
+            font-size: 16px;
+            cursor: pointer;
+            border: none;
+            border-radius: 5px;
+            background: #fff;
+            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+            transition: transform 0.1s;
+        }
+        button:active {
+            transform: scale(0.98);
+        }
+        #result {
+            margin-top: 30px;
+            font-size: 18px;
+            color: #555;
+            height: 30px;
+        }
+    </style>
+</head>
+<body>
+
+    <h1>Layer.js Test</h1>
+
+    <div class="btn-group">
+        <button onclick="testBasic()">Basic Message</button>
+        <button onclick="testSuccess()">Success Icon</button>
+        <button onclick="testError()">Error Icon</button>
+        <button onclick="testWarning()">Warning Icon</button>
+        <button onclick="testConfirm()">Confirm Dialog</button>
+        <button onclick="testChaining()">Chain Promises</button>
+    </div>
+
+    <div id="result"></div>
+
+    <script src="layer.js"></script>
+    <script>
+        function log(msg) {
+            document.getElementById('result').textContent = msg;
+            console.log(msg);
+        }
+
+        function testBasic() {
+            Layer.fire('Hello World');
+        }
+
+        function testSuccess() {
+            Layer.fire('Good job!', 'You clicked the button!', 'success');
+        }
+
+        function testError() {
+            Layer.fire({
+                icon: 'error',
+                title: 'Oops...',
+                text: 'Something went wrong!',
+            });
+        }
+
+        function testWarning() {
+            Layer.fire({
+                title: 'Are you sure?',
+                text: "You won't be able to revert this!",
+                icon: 'warning',
+                showCancelButton: true,
+                confirmButtonColor: '#3085d6',
+                cancelButtonColor: '#d33',
+                confirmButtonText: 'Yes, delete it!'
+            }).then((result) => {
+                if (result.isConfirmed) {
+                    Layer.fire(
+                        'Deleted!',
+                        'Your file has been deleted.',
+                        'success'
+                    )
+                }
+            });
+        }
+
+        function testConfirm() {
+            Layer.fire({
+                title: 'Do you want to save the changes?',
+                showCancelButton: true,
+                confirmButtonText: 'Save',
+            }).then((result) => {
+                if (result.isConfirmed) {
+                    log('Saved!');
+                } else if (result.isDismissed) {
+                    log('Changes are not saved');
+                }
+            });
+        }
+
+        function testChaining() {
+            Layer.fire({
+                title: 'Step 1',
+                text: 'Proceed to step 2?',
+                showCancelButton: true
+            }).then((result) => {
+                if (result.isConfirmed) {
+                    return Layer.fire({
+                        title: 'Step 2',
+                        text: 'Last step!',
+                        icon: 'success'
+                    });
+                }
+            }).then(() => {
+                log('Process completed');
+            });
+        }
+    </script>
+</body>
+</html>
+

+ 0 - 0
xjs.js