robert пре 2 дана
родитељ
комит
0a5d62e939

+ 30 - 2
animal.js

@@ -551,8 +551,25 @@
         if (k in t.style) return t.style[k];
       }
       
-      // SVG attribute
+      // SVG
+      // For many SVG presentation attributes (e.g. strokeDashoffset), style overrides attribute.
+      // Prefer computed style / inline style when available, and fallback to attributes.
       if (isSVG(t)) {
+        // Geometry attrs must remain attrs (not style)
+        if (k === 'd' || k === 'points') {
+          const attr = toKebab(k);
+          if (t.hasAttribute(attr)) return t.getAttribute(attr);
+          if (t.hasAttribute(k)) return t.getAttribute(k);
+          return t.getAttribute(k);
+        }
+        try {
+          const cs = getComputedStyle(t);
+          const v = cs.getPropertyValue(toKebab(k));
+          if (v && v.trim()) return v.trim();
+        } catch (_) {}
+        try {
+          if (k in t.style) return t.style[k];
+        } catch (_) {}
         const attr = toKebab(k);
         if (t.hasAttribute(attr)) return t.getAttribute(attr);
         if (t.hasAttribute(k)) return t.getAttribute(k);
@@ -588,8 +605,19 @@
         return;
       }
       
-      // SVG attribute
+      // SVG
+      // Prefer style for presentation attrs so it can animate when svgDraw initialized styles.
       if (isSVG(t)) {
+        if (k === 'd' || k === 'points') {
+          t.setAttribute(k, v);
+          return;
+        }
+        try {
+          if (k in t.style) {
+            t.style[k] = v;
+            return;
+          }
+        } catch (_) {}
         const attr = toKebab(k);
         t.setAttribute(attr, v);
         return;

+ 8 - 0
doc/index.html

@@ -240,6 +240,7 @@
             <div class="nav-item" onclick="selectCategory('examples/list.html', 'examples/controls.html', this)">示例 Examples</div>
 
             <div class="nav-section-title">Layer 弹窗</div>
+            <div class="nav-item" onclick="selectCategory('layer/list.html', 'layer/overview.html', this)">Layer API / Tests</div>
             <div class="nav-item" onclick="selectCategory('examples/list.html', 'examples/layer.html', this)">交互示例(Layer + 动画)</div>
 
             <div class="nav-section-title">开发者</div>
@@ -362,6 +363,13 @@
             { group: 'Examples', title: 'Controls', url: 'examples/controls.html' },
             { group: 'Examples', title: 'Layer', url: 'examples/layer.html' },
 
+            // Layer
+            { group: 'Layer', title: 'Layer 概览', url: 'layer/overview.html' },
+            { group: 'Layer', title: 'SVG Icon animation', url: 'layer/test_icons_svg_animation.html' },
+            { group: 'Layer', title: 'Confirm flow', url: 'layer/test_confirm_flow.html' },
+            { group: 'Layer', title: 'Selection.layer + dataset', url: 'layer/test_selection_layer_dataset.html' },
+            { group: 'Layer', title: 'Static API / Builder API', url: 'layer/test_static_fire.html' },
+
             // Developer
             { group: 'Developer', title: '模块开发与测试指南', url: 'module.html' }
         ];

+ 158 - 0
doc/layer/list.html

@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Layer - List</title>
+  <link rel="stylesheet" href="../list.css">
+  <style>
+    .card { min-height: 120px; padding: 18px 18px 16px 18px; }
+    .card-preview {
+      height: 62px;
+      border-radius: 10px;
+      background: rgba(255,255,255,0.02);
+      border: 1px solid rgba(255,255,255,0.04);
+      position: relative;
+      overflow: hidden;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-bottom: 14px;
+    }
+    .preview-icons {
+      display: flex;
+      gap: 10px;
+      align-items: center;
+      justify-content: center;
+      opacity: 0.9;
+    }
+    .mini {
+      width: 22px;
+      height: 22px;
+      border-radius: 999px;
+      border: 2px solid currentColor;
+      display: grid;
+      place-items: center;
+      color: #aaa;
+    }
+    .mini.success { color: #a5dc86; }
+    .mini.error { color: #f27474; }
+    .mini.warning { color: #f8bb86; }
+    .mini.info { color: #3fc3ee; }
+    .mini svg { width: 14px; height: 14px; display: block; }
+    .preview-pill {
+      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+      color: #9a9a9a;
+      background: rgba(255,255,255,0.04);
+      border: 1px solid rgba(255,255,255,0.06);
+      padding: 10px 14px;
+      border-radius: 10px;
+      font-size: 13px;
+      letter-spacing: 0.2px;
+    }
+  </style>
+</head>
+<body>
+  <div class="search-header">
+    <div class="search-title">Layer</div>
+    <div class="search-input-container">
+      <div class="search-icon"></div>
+      <input type="text" class="search-input" placeholder="Filter layer tests..." disabled>
+    </div>
+  </div>
+
+  <div class="grid-section">
+    <div class="header-card active" onclick="selectItem('overview.html', this)">
+      <div class="header-dots"></div>
+      <div class="header-title">Layer</div>
+    </div>
+
+    <div class="card-grid" style="margin-top: 40px;">
+      <div class="card" data-content="layer/test_icons_svg_animation.html" onclick="selectItem('test_icons_svg_animation.html', this)">
+        <div class="card-preview">
+          <div class="preview-icons" aria-hidden="true">
+            <div class="mini success">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">
+                <path d="M20 6L9 17l-5-5"></path>
+              </svg>
+            </div>
+            <div class="mini error">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round">
+                <path d="M6 6l12 12"></path>
+                <path d="M18 6L6 18"></path>
+              </svg>
+            </div>
+            <div class="mini warning">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round">
+                <path d="M12 3v12"></path>
+                <path d="M12 19h.01"></path>
+              </svg>
+            </div>
+            <div class="mini info">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round">
+                <path d="M12 10v8"></path>
+                <path d="M12 6h.01"></path>
+              </svg>
+            </div>
+          </div>
+        </div>
+        <div class="card-footer">SVG Icon animation (SweetAlert-like)</div>
+      </div>
+
+      <div class="card" data-content="layer/test_confirm_flow.html" onclick="selectItem('test_confirm_flow.html', this)">
+        <div class="card-preview">
+          <div class="preview-pill">xjs.layer(...).then(res =&gt; ...)</div>
+        </div>
+        <div class="card-footer">Confirm flow (success / error)</div>
+      </div>
+
+      <div class="card" data-content="layer/test_selection_layer_dataset.html" onclick="selectItem('test_selection_layer_dataset.html', this)">
+        <div class="card-preview">
+          <div class="preview-pill">xjs('.btn').layer() + data-layer-*</div>
+        </div>
+        <div class="card-footer">Selection.layer() + dataset</div>
+      </div>
+
+      <div class="card" data-content="layer/test_static_fire.html" onclick="selectItem('test_static_fire.html', this)">
+        <div class="card-preview">
+          <div class="preview-pill">Layer.fire(...) / Layer.$(...).fire()</div>
+        </div>
+        <div class="card-footer">Static API / Builder API</div>
+      </div>
+    </div>
+  </div>
+
+  <script>
+    function selectItem(url, el) {
+      document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
+      document.querySelectorAll('.header-card').forEach(c => c.classList.remove('active'));
+      el.classList.add('active');
+      if (window.parent && window.parent.loadContent) {
+        window.parent.loadContent('layer/' + url);
+      }
+    }
+
+    function setActiveByContentUrl(contentUrl) {
+      const normalized = String(contentUrl || '').replace(/^\.\//, '');
+      const header = document.querySelector('.header-card');
+      const cards = Array.from(document.querySelectorAll('.card[data-content]'));
+
+      document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
+      document.querySelectorAll('.header-card').forEach(c => c.classList.remove('active'));
+
+      if (!normalized) return;
+      if (normalized.includes('layer/overview.html')) {
+        header?.classList.add('active');
+        return;
+      }
+
+      header?.classList.add('active');
+      const match = cards.find(c => normalized.endsWith(c.dataset.content) || normalized.includes(c.dataset.content));
+      if (match) match.classList.add('active');
+    }
+
+    window.setActiveByContentUrl = setActiveByContentUrl;
+  </script>
+</body>
+</html>
+

+ 200 - 0
doc/layer/overview.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Layer - Overview</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      align-items: center;
+    }
+    .btn {
+      appearance: none;
+      border: 1px solid rgba(255,255,255,0.12);
+      background: rgba(255,255,255,0.04);
+      color: #fff;
+      border-radius: 999px;
+      padding: 10px 14px;
+      font-weight: 650;
+      cursor: pointer;
+      transition: border-color 0.15s ease, background 0.15s ease;
+    }
+    .btn:hover { border-color: rgba(255,255,255,0.22); background: rgba(255,255,255,0.06); }
+    .btn.success { border-color: rgba(165,220,134,0.55); background: rgba(165,220,134,0.12); }
+    .btn.error { border-color: rgba(242,116,116,0.55); background: rgba(242,116,116,0.12); }
+    .btn.warning { border-color: rgba(248,187,134,0.55); background: rgba(248,187,134,0.12); }
+    .btn.info { border-color: rgba(63,195,238,0.55); background: rgba(63,195,238,0.12); }
+    .hint { font-size: 12px; color: #8c8c8c; line-height: 1.5; }
+    .demo-visual { padding: 22px 24px; }
+    .kv {
+      display: grid;
+      grid-template-columns: 160px 1fr;
+      gap: 10px 14px;
+      font-size: 13px;
+      color: #b7b7b7;
+      margin-top: 10px;
+    }
+    .kv code { color: #e6e6e6; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">UI › LAYER</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>Layer</h1>
+    <p class="description">
+      Minimal popup API with <code class="inline">Promise</code> result, plus SweetAlert-like SVG icon animations powered by <code class="inline">xjs.draw()</code> / spring easing.
+    </p>
+
+    <h2>Quick demo</h2>
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">Open popups</div>
+        <div class="tabs">
+          <div class="tab active" onclick="switchTab('js')">JavaScript</div>
+          <div class="tab" onclick="switchTab('html')">HTML</div>
+          <div class="tab" onclick="switchTab('css')">CSS</div>
+        </div>
+      </div>
+
+      <pre id="js-code" class="code-view active">// SweetAlert-like icons (SVG + draw)
+xjs.layer({ title: 'Success', text: 'Saved!', icon: 'success' });
+xjs.layer({ title: 'Error', text: 'Something went wrong', icon: 'error' });
+
+// Confirm flow
+xjs.layer({
+  title: 'Delete item?',
+  text: 'This action cannot be undone.',
+  icon: 'warning',
+  showCancelButton: true
+}).then((res) => {
+  if (res.isConfirmed) xjs.layer({ title: 'Deleted', icon: 'success' });
+});</pre>
+
+      <pre id="html-code" class="html-view">&lt;button class="btn success"&gt;Success&lt;/button&gt;
+&lt;button class="btn error"&gt;Error&lt;/button&gt;
+&lt;button class="btn warning"&gt;Warning&lt;/button&gt;
+&lt;button class="btn info"&gt;Info&lt;/button&gt;
+&lt;button class="btn"&gt;Confirm&lt;/button&gt;</pre>
+
+      <pre id="css-code" class="css-view">/* This page's button styles are local.
+   Layer's own CSS is injected by layer.js */</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>Layer 的 icon 现在是内联 SVG(ring + mark),打开弹窗时会自动触发描边动画;弹窗支持 <code class="inline">ESC</code> 关闭(可用 <code class="inline">closeOnEsc:false</code> 关闭)。
+      </div>
+
+      <div class="demo-visual">
+        <div class="row">
+          <button class="btn success" onclick="openIcon('success')">Success</button>
+          <button class="btn error" onclick="openIcon('error')">Error</button>
+          <button class="btn warning" onclick="openIcon('warning')">Warning</button>
+          <button class="btn info" onclick="openIcon('info')">Info</button>
+          <button class="btn" onclick="openConfirm()">Confirm</button>
+        </div>
+        <div class="hint" style="margin-top: 12px;">
+          Tip: try pressing <code class="inline">ESC</code> while the popup is open.
+        </div>
+        <div class="kv">
+          <div>icon</div><div><code class="inline">success | error | warning | info</code></div>
+          <div>iconAnimation</div><div><code class="inline">true</code> (default)</div>
+          <div>popupAnimation</div><div><code class="inline">true</code> (default)</div>
+          <div>closeOnEsc</div><div><code class="inline">true</code> (default)</div>
+        </div>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" id="prevLink" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span class="nav-title" id="prevTitle">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div class="nav-center" id="navCenter">Layer</div>
+      <a href="#" id="nextLink" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span class="nav-title" id="nextTitle">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <script src="../highlight_css.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'layer/overview.html';
+
+    function switchTab(tab) {
+      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+      document.querySelectorAll('.code-view, .html-view, .css-view').forEach(v => v.classList.remove('active'));
+      if (tab === 'js') {
+        document.querySelector('.tabs .tab:nth-child(1)')?.classList.add('active');
+        document.getElementById('js-code')?.classList.add('active');
+      } else if (tab === 'html') {
+        document.querySelector('.tabs .tab:nth-child(2)')?.classList.add('active');
+        document.getElementById('html-code')?.classList.add('active');
+      } else {
+        document.querySelector('.tabs .tab:nth-child(3)')?.classList.add('active');
+        document.getElementById('css-code')?.classList.add('active');
+      }
+    }
+
+    function openIcon(type) {
+      xjs.layer({
+        title: type[0].toUpperCase() + type.slice(1),
+        text: 'SVG icon draw + spring entrance',
+        icon: type
+      });
+    }
+
+    function openConfirm() {
+      xjs.layer({
+        title: 'Delete item?',
+        text: 'This action cannot be undone.',
+        icon: 'warning',
+        showCancelButton: true,
+        confirmButtonText: 'Delete',
+        cancelButtonText: 'Cancel'
+      }).then((res) => {
+        if (res.isConfirmed) {
+          xjs.layer({ title: 'Deleted', text: 'Done', icon: 'success' });
+        } else if (res.dismiss === 'cancel') {
+          xjs.layer({ title: 'Cancelled', text: 'Nothing happened', icon: 'info' });
+        }
+      });
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    function syncNavLabels() {
+      const api = window.parent?.docGetPrevNext;
+      if (typeof api !== 'function') return;
+      const { prev, next, current } = api(CURRENT) || {};
+      const prevLink = document.getElementById('prevLink');
+      const nextLink = document.getElementById('nextLink');
+      if (prev) document.getElementById('prevTitle').textContent = prev.title || prev.url;
+      else { prevLink.style.visibility = 'hidden'; }
+      if (next) document.getElementById('nextTitle').textContent = next.title || next.url;
+      else { nextLink.style.visibility = 'hidden'; }
+      document.getElementById('navCenter').textContent = (current && current.group) ? current.group : 'Layer';
+    }
+
+    // Sync middle list selection when loaded inside doc/index.html
+    try { window.parent?.setMiddleActive?.(CURRENT); } catch {}
+    syncNavLabels();
+  </script>
+</body>
+</html>
+

+ 167 - 0
doc/layer/test_confirm_flow.html

@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Layer Confirm Flow - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      align-items: center;
+    }
+    .btn {
+      appearance: none;
+      border: 1px solid rgba(255,255,255,0.12);
+      background: rgba(255,255,255,0.04);
+      color: #fff;
+      border-radius: 999px;
+      padding: 10px 14px;
+      font-weight: 650;
+      cursor: pointer;
+      transition: border-color 0.15s ease, background 0.15s ease;
+    }
+    .btn:hover { border-color: rgba(255,255,255,0.22); background: rgba(255,255,255,0.06); }
+    .btn.danger { border-color: rgba(255,75,75,0.55); background: rgba(255,75,75,0.12); }
+    .btn.danger:hover { border-color: rgba(255,75,75,0.7); background: rgba(255,75,75,0.18); }
+    .demo-visual { padding: 22px 24px; }
+    .hint { font-size: 12px; color: #8c8c8c; line-height: 1.5; margin-top: 10px; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">UI › LAYER</div>
+      <div class="since">CONFIRM FLOW</div>
+    </div>
+
+    <h1>Confirm flow</h1>
+    <p class="description">
+      The promise resolves with <code class="inline">{ isConfirmed, isDismissed, dismiss }</code>. This page shows how to chain a follow-up popup (success / info / error).
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">Example</div>
+        <div class="tabs">
+          <div class="tab active" onclick="switchTab('js')">JavaScript</div>
+          <div class="tab" onclick="switchTab('html')">HTML</div>
+          <div class="tab" onclick="switchTab('css')">CSS</div>
+        </div>
+      </div>
+
+      <pre id="js-code" class="code-view active">xjs.layer({
+  title: 'Delete item?',
+  text: 'This action cannot be undone.',
+  icon: 'warning',
+  showCancelButton: true,
+  confirmButtonText: 'Delete',
+  cancelButtonText: 'Cancel'
+}).then((res) => {
+  if (res.isConfirmed) {
+    xjs.layer({ title: 'Deleted', icon: 'success' });
+  } else {
+    xjs.layer({ title: 'Cancelled', icon: 'info' });
+  }
+});</pre>
+
+      <pre id="html-code" class="html-view">&lt;button class="btn danger"&gt;Open confirm&lt;/button&gt;</pre>
+      <pre id="css-code" class="css-view">/* Local styles for buttons only */</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>验证 Layer 的 Promise 结果结构,以及多弹窗串联时的体验(warning → success/info)。
+      </div>
+
+      <div class="demo-visual">
+        <div class="row">
+          <button class="btn danger" onclick="openConfirm()">Open confirm</button>
+          <button class="btn" onclick="openError()">Open error</button>
+        </div>
+        <div class="hint">
+          “Open error” shows the staggered X mark animation.
+        </div>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" id="prevLink" onclick="goPrev(); return false;">
+        <span><span class="nav-label">Previous</span><br><span class="nav-title" id="prevTitle">—</span></span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div class="nav-center" id="navCenter">Layer</div>
+      <a href="#" id="nextLink" onclick="goNext(); return false;">
+        <span><span class="nav-label">Next</span><br><span class="nav-title" id="nextTitle">—</span></span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <script src="../highlight_css.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'layer/test_confirm_flow.html';
+
+    function switchTab(tab) {
+      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+      document.querySelectorAll('.code-view, .html-view, .css-view').forEach(v => v.classList.remove('active'));
+      if (tab === 'js') {
+        document.querySelector('.tabs .tab:nth-child(1)')?.classList.add('active');
+        document.getElementById('js-code')?.classList.add('active');
+      } else if (tab === 'html') {
+        document.querySelector('.tabs .tab:nth-child(2)')?.classList.add('active');
+        document.getElementById('html-code')?.classList.add('active');
+      } else {
+        document.querySelector('.tabs .tab:nth-child(3)')?.classList.add('active');
+        document.getElementById('css-code')?.classList.add('active');
+      }
+    }
+
+    function openConfirm() {
+      xjs.layer({
+        title: 'Delete item?',
+        text: 'This action cannot be undone.',
+        icon: 'warning',
+        showCancelButton: true,
+        confirmButtonText: 'Delete',
+        cancelButtonText: 'Cancel',
+        confirmButtonColor: '#d33',
+        cancelButtonColor: '#444'
+      }).then((res) => {
+        if (res.isConfirmed) {
+          xjs.layer({ title: 'Deleted', text: 'Done', icon: 'success' });
+        } else if (res.dismiss === 'cancel') {
+          xjs.layer({ title: 'Cancelled', text: 'No changes', icon: 'info' });
+        } else {
+          xjs.layer({ title: 'Dismissed', text: 'Backdrop / ESC', icon: 'info' });
+        }
+      });
+    }
+
+    function openError() {
+      xjs.layer({ title: 'Error', text: 'Example error popup', icon: 'error' });
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    function syncNavLabels() {
+      const api = window.parent?.docGetPrevNext;
+      if (typeof api !== 'function') return;
+      const { prev, next, current } = api(CURRENT) || {};
+      const prevLink = document.getElementById('prevLink');
+      const nextLink = document.getElementById('nextLink');
+      if (prev) document.getElementById('prevTitle').textContent = prev.title || prev.url;
+      else { prevLink.style.visibility = 'hidden'; }
+      if (next) document.getElementById('nextTitle').textContent = next.title || next.url;
+      else { nextLink.style.visibility = 'hidden'; }
+      document.getElementById('navCenter').textContent = (current && current.group) ? current.group : 'Layer';
+    }
+
+    try { window.parent?.setMiddleActive?.(CURRENT); } catch {}
+    syncNavLabels();
+  </script>
+</body>
+</html>
+

+ 176 - 0
doc/layer/test_icons_svg_animation.html

@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Layer SVG Icon Animation - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .controls {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px 16px;
+      align-items: center;
+      font-size: 13px;
+      color: #b7b7b7;
+    }
+    .controls label {
+      display: inline-flex;
+      gap: 8px;
+      align-items: center;
+      cursor: pointer;
+      user-select: none;
+    }
+    .controls input { accent-color: var(--highlight-color); }
+    .row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      align-items: center;
+    }
+    .btn {
+      appearance: none;
+      border: 1px solid rgba(255,255,255,0.12);
+      background: rgba(255,255,255,0.04);
+      color: #fff;
+      border-radius: 999px;
+      padding: 10px 14px;
+      font-weight: 650;
+      cursor: pointer;
+      transition: border-color 0.15s ease, background 0.15s ease;
+    }
+    .btn:hover { border-color: rgba(255,255,255,0.22); background: rgba(255,255,255,0.06); }
+    .btn.success { border-color: rgba(165,220,134,0.55); background: rgba(165,220,134,0.12); }
+    .btn.error { border-color: rgba(242,116,116,0.55); background: rgba(242,116,116,0.12); }
+    .btn.warning { border-color: rgba(248,187,134,0.55); background: rgba(248,187,134,0.12); }
+    .btn.info { border-color: rgba(63,195,238,0.55); background: rgba(63,195,238,0.12); }
+    .demo-visual { padding: 22px 24px; }
+    .hint { font-size: 12px; color: #8c8c8c; line-height: 1.5; margin-top: 10px; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">UI › LAYER</div>
+      <div class="since">ICON ANIMATION</div>
+    </div>
+
+    <h1>SVG Icon animation</h1>
+    <p class="description">
+      Uses <code class="inline">xjs.draw()</code> to animate the ring + mark strokes, and a spring entrance to mimic SweetAlert-like feedback.
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">Controls</div>
+        <div class="tabs">
+          <div class="tab active" onclick="switchTab('js')">JavaScript</div>
+          <div class="tab" onclick="switchTab('html')">HTML</div>
+          <div class="tab" onclick="switchTab('css')">CSS</div>
+        </div>
+      </div>
+
+      <pre id="js-code" class="code-view active">xjs.layer({
+  title: 'Success',
+  text: 'SVG draw + spring',
+  icon: 'success',
+  iconAnimation: true,
+  popupAnimation: true
+});</pre>
+
+      <pre id="html-code" class="html-view">&lt;label&gt;&lt;input type="checkbox" checked&gt; iconAnimation&lt;/label&gt;
+&lt;label&gt;&lt;input type="checkbox" checked&gt; popupAnimation&lt;/label&gt;</pre>
+
+      <pre id="css-code" class="css-view">/* Icon styling lives inside layer.js */</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>本页专门验证 Layer 的 icon 动画:描边(draw)+ 弹性缩放(spring)。你可以关闭其中任意一项来对比体验。
+      </div>
+
+      <div class="demo-visual">
+        <div class="controls" style="margin-bottom: 14px;">
+          <label><input id="optIcon" type="checkbox" checked> iconAnimation</label>
+          <label><input id="optPopup" type="checkbox" checked> popupAnimation</label>
+          <label><input id="optEsc" type="checkbox" checked> closeOnEsc</label>
+        </div>
+        <div class="row">
+          <button class="btn success" onclick="open('success')">Success</button>
+          <button class="btn error" onclick="open('error')">Error</button>
+          <button class="btn warning" onclick="open('warning')">Warning</button>
+          <button class="btn info" onclick="open('info')">Info</button>
+        </div>
+        <div class="hint">
+          Tip: open “Error” to see the two stroke segments staggered.
+        </div>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" id="prevLink" onclick="goPrev(); return false;">
+        <span><span class="nav-label">Previous</span><br><span class="nav-title" id="prevTitle">—</span></span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div class="nav-center" id="navCenter">Layer</div>
+      <a href="#" id="nextLink" onclick="goNext(); return false;">
+        <span><span class="nav-label">Next</span><br><span class="nav-title" id="nextTitle">—</span></span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <script src="../highlight_css.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'layer/test_icons_svg_animation.html';
+
+    function switchTab(tab) {
+      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+      document.querySelectorAll('.code-view, .html-view, .css-view').forEach(v => v.classList.remove('active'));
+      if (tab === 'js') {
+        document.querySelector('.tabs .tab:nth-child(1)')?.classList.add('active');
+        document.getElementById('js-code')?.classList.add('active');
+      } else if (tab === 'html') {
+        document.querySelector('.tabs .tab:nth-child(2)')?.classList.add('active');
+        document.getElementById('html-code')?.classList.add('active');
+      } else {
+        document.querySelector('.tabs .tab:nth-child(3)')?.classList.add('active');
+        document.getElementById('css-code')?.classList.add('active');
+      }
+    }
+
+    function open(type) {
+      const iconAnimation = !!document.getElementById('optIcon')?.checked;
+      const popupAnimation = !!document.getElementById('optPopup')?.checked;
+      const closeOnEsc = !!document.getElementById('optEsc')?.checked;
+      xjs.layer({
+        title: type[0].toUpperCase() + type.slice(1),
+        text: 'SVG draw + spring entrance',
+        icon: type,
+        iconAnimation,
+        popupAnimation,
+        closeOnEsc
+      });
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    function syncNavLabels() {
+      const api = window.parent?.docGetPrevNext;
+      if (typeof api !== 'function') return;
+      const { prev, next, current } = api(CURRENT) || {};
+      const prevLink = document.getElementById('prevLink');
+      const nextLink = document.getElementById('nextLink');
+      if (prev) document.getElementById('prevTitle').textContent = prev.title || prev.url;
+      else { prevLink.style.visibility = 'hidden'; }
+      if (next) document.getElementById('nextTitle').textContent = next.title || next.url;
+      else { nextLink.style.visibility = 'hidden'; }
+      document.getElementById('navCenter').textContent = (current && current.group) ? current.group : 'Layer';
+    }
+
+    try { window.parent?.setMiddleActive?.(CURRENT); } catch {}
+    syncNavLabels();
+  </script>
+</body>
+</html>
+

+ 184 - 0
doc/layer/test_selection_layer_dataset.html

@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Selection.layer() + dataset - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px;
+      align-items: center;
+    }
+    .btn {
+      appearance: none;
+      border: 1px solid rgba(255,255,255,0.12);
+      background: rgba(255,255,255,0.04);
+      color: #fff;
+      border-radius: 999px;
+      padding: 10px 14px;
+      font-weight: 650;
+      cursor: pointer;
+      transition: border-color 0.15s ease, background 0.15s ease;
+    }
+    .btn:hover { border-color: rgba(255,255,255,0.22); background: rgba(255,255,255,0.06); }
+    .btn.danger { border-color: rgba(255,75,75,0.55); background: rgba(255,75,75,0.12); }
+    .btn.danger:hover { border-color: rgba(255,75,75,0.7); background: rgba(255,75,75,0.18); }
+    .pill {
+      display: inline-flex;
+      align-items: center;
+      gap: 8px;
+      border: 1px solid rgba(255,255,255,0.08);
+      background: rgba(255,255,255,0.03);
+      border-radius: 999px;
+      padding: 8px 12px;
+      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+      font-size: 12px;
+      color: #b7b7b7;
+      overflow-x: auto;
+      max-width: 100%;
+      white-space: nowrap;
+    }
+    .demo-visual { padding: 22px 24px; }
+    .hint { font-size: 12px; color: #8c8c8c; line-height: 1.5; margin-top: 12px; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">UI › LAYER</div>
+      <div class="since">BINDING</div>
+    </div>
+
+    <h1>Selection.layer() + dataset</h1>
+    <p class="description">
+      Bind click-to-popup via <code class="inline">xjs('.btn').layer(options)</code>, or via <code class="inline">data-layer-*</code> attributes.
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">Binding example</div>
+        <div class="tabs">
+          <div class="tab active" onclick="switchTab('js')">JavaScript</div>
+          <div class="tab" onclick="switchTab('html')">HTML</div>
+          <div class="tab" onclick="switchTab('css')">CSS</div>
+        </div>
+      </div>
+
+      <pre id="js-code" class="code-view active">// 1) Bind with explicit options
+xjs('.btn-delete').layer({
+  title: 'Delete item?',
+  text: 'This action cannot be undone.',
+  icon: 'warning',
+  showCancelButton: true
+});
+
+// 2) Bind from data-* (no JS options needed)
+xjs('.btn-data').layer();</pre>
+
+      <pre id="html-code" class="html-view">&lt;button class="btn danger btn-delete"&gt;Delete (bind .layer)&lt;/button&gt;
+
+&lt;button
+  class="btn btn-data"
+  data-layer-title="Data-driven popup"
+  data-layer-text="Configured via data-layer-* attributes"
+  data-layer-icon="success"
+  data-layer-cancel="true"
+&gt;Open (data-layer-*)&lt;/button&gt;</pre>
+
+      <pre id="css-code" class="css-view">/* Local styles for buttons only */</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>验证 <code class="inline">Selection.layer()</code> 的去重绑定与 dataset 解析(<code class="inline">data-layer-title/text/icon/cancel</code>)。
+      </div>
+
+      <div class="demo-visual">
+        <div class="row">
+          <button class="btn danger btn-delete">Delete (bind .layer)</button>
+          <button
+            class="btn btn-data"
+            data-layer-title="Data-driven popup"
+            data-layer-text="Configured via data-layer-* attributes"
+            data-layer-icon="success"
+            data-layer-cancel="true"
+          >Open (data-layer-*)</button>
+        </div>
+        <div class="hint">
+          Re-binding calls should replace old handlers automatically.
+          <span class="pill">xjs('.btn').layer(options)</span>
+        </div>
+      </div>
+
+      <div class="action-bar">
+        <button class="play-btn" onclick="bindAll()">RE-BIND</button>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" id="prevLink" onclick="goPrev(); return false;">
+        <span><span class="nav-label">Previous</span><br><span class="nav-title" id="prevTitle">—</span></span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div class="nav-center" id="navCenter">Layer</div>
+      <a href="#" id="nextLink" onclick="goNext(); return false;">
+        <span><span class="nav-label">Next</span><br><span class="nav-title" id="nextTitle">—</span></span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <script src="../highlight_css.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'layer/test_selection_layer_dataset.html';
+
+    function switchTab(tab) {
+      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+      document.querySelectorAll('.code-view, .html-view, .css-view').forEach(v => v.classList.remove('active'));
+      if (tab === 'js') {
+        document.querySelector('.tabs .tab:nth-child(1)')?.classList.add('active');
+        document.getElementById('js-code')?.classList.add('active');
+      } else if (tab === 'html') {
+        document.querySelector('.tabs .tab:nth-child(2)')?.classList.add('active');
+        document.getElementById('html-code')?.classList.add('active');
+      } else {
+        document.querySelector('.tabs .tab:nth-child(3)')?.classList.add('active');
+        document.getElementById('css-code')?.classList.add('active');
+      }
+    }
+
+    function bindAll() {
+      xjs('.btn-delete').layer({
+        title: 'Delete item?',
+        text: 'This action cannot be undone.',
+        icon: 'warning',
+        showCancelButton: true
+      });
+      xjs('.btn-data').layer(); // reads data-layer-* attributes
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    function syncNavLabels() {
+      const api = window.parent?.docGetPrevNext;
+      if (typeof api !== 'function') return;
+      const { prev, next, current } = api(CURRENT) || {};
+      const prevLink = document.getElementById('prevLink');
+      const nextLink = document.getElementById('nextLink');
+      if (prev) document.getElementById('prevTitle').textContent = prev.title || prev.url;
+      else { prevLink.style.visibility = 'hidden'; }
+      if (next) document.getElementById('nextTitle').textContent = next.title || next.url;
+      else { nextLink.style.visibility = 'hidden'; }
+      document.getElementById('navCenter').textContent = (current && current.group) ? current.group : 'Layer';
+    }
+
+    try { window.parent?.setMiddleActive?.(CURRENT); } catch {}
+    syncNavLabels();
+    setTimeout(bindAll, 250);
+  </script>
+</body>
+</html>
+

+ 159 - 0
doc/layer/test_static_fire.html

@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Layer Static API - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      align-items: center;
+    }
+    .btn {
+      appearance: none;
+      border: 1px solid rgba(255,255,255,0.12);
+      background: rgba(255,255,255,0.04);
+      color: #fff;
+      border-radius: 999px;
+      padding: 10px 14px;
+      font-weight: 650;
+      cursor: pointer;
+      transition: border-color 0.15s ease, background 0.15s ease;
+    }
+    .btn:hover { border-color: rgba(255,255,255,0.22); background: rgba(255,255,255,0.06); }
+    .btn.primary { border-color: rgba(48,133,214,0.55); background: rgba(48,133,214,0.18); }
+    .btn.primary:hover { border-color: rgba(48,133,214,0.8); background: rgba(48,133,214,0.24); }
+    .demo-visual { padding: 22px 24px; }
+    .hint { font-size: 12px; color: #8c8c8c; line-height: 1.5; margin-top: 10px; }
+    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">UI › LAYER</div>
+      <div class="since">STATIC API</div>
+    </div>
+
+    <h1>Static API / Builder API</h1>
+    <p class="description">
+      Layer supports <code class="inline">Layer.fire()</code> and <code class="inline">Layer.$(...).fire()</code> (builder style), and returns a Promise result.
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">Examples</div>
+        <div class="tabs">
+          <div class="tab active" onclick="switchTab('js')">JavaScript</div>
+          <div class="tab" onclick="switchTab('html')">HTML</div>
+          <div class="tab" onclick="switchTab('css')">CSS</div>
+        </div>
+      </div>
+
+      <pre id="js-code" class="code-view active">// 1) Static fire
+Layer.fire({ title: 'Hello', text: 'Layer.fire(...)', icon: 'info' });
+
+// 2) Builder style
+Layer.$()
+  .config({ title: 'Confirm?', icon: 'warning', showCancelButton: true })
+  .fire()
+  .then((res) => console.log(res));</pre>
+
+      <pre id="html-code" class="html-view">&lt;button class="btn primary"&gt;Layer.fire()&lt;/button&gt;
+&lt;button class="btn"&gt;Layer.$().config().fire()&lt;/button&gt;</pre>
+
+      <pre id="css-code" class="css-view">/* Local styles for buttons only */</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>验证 Layer 自身的静态入口(不依赖 Selection.layer 绑定),以及 builder 模式的链式配置。
+      </div>
+
+      <div class="demo-visual">
+        <div class="row">
+          <button class="btn primary" onclick="openFire()">Layer.fire()</button>
+          <button class="btn" onclick="openBuilder()">Builder fire()</button>
+        </div>
+        <div class="hint">
+          Promise result is printed to <span class="mono">console</span>.
+        </div>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" id="prevLink" onclick="goPrev(); return false;">
+        <span><span class="nav-label">Previous</span><br><span class="nav-title" id="prevTitle">—</span></span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div class="nav-center" id="navCenter">Layer</div>
+      <a href="#" id="nextLink" onclick="goNext(); return false;">
+        <span><span class="nav-label">Next</span><br><span class="nav-title" id="nextTitle">—</span></span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <script src="../highlight_css.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'layer/test_static_fire.html';
+
+    function switchTab(tab) {
+      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+      document.querySelectorAll('.code-view, .html-view, .css-view').forEach(v => v.classList.remove('active'));
+      if (tab === 'js') {
+        document.querySelector('.tabs .tab:nth-child(1)')?.classList.add('active');
+        document.getElementById('js-code')?.classList.add('active');
+      } else if (tab === 'html') {
+        document.querySelector('.tabs .tab:nth-child(2)')?.classList.add('active');
+        document.getElementById('html-code')?.classList.add('active');
+      } else {
+        document.querySelector('.tabs .tab:nth-child(3)')?.classList.add('active');
+        document.getElementById('css-code')?.classList.add('active');
+      }
+    }
+
+    function openFire() {
+      Layer.fire({ title: 'Hello', text: 'Layer.fire(...)', icon: 'info' })
+        .then((res) => console.log('[Layer.fire] result:', res));
+    }
+
+    function openBuilder() {
+      Layer.$()
+        .config({
+          title: 'Confirm?',
+          text: 'Builder style API',
+          icon: 'warning',
+          showCancelButton: true
+        })
+        .fire()
+        .then((res) => {
+          console.log('[Layer.builder] result:', res);
+          if (res.isConfirmed) xjs.layer({ title: 'Confirmed', icon: 'success' });
+        });
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    function syncNavLabels() {
+      const api = window.parent?.docGetPrevNext;
+      if (typeof api !== 'function') return;
+      const { prev, next, current } = api(CURRENT) || {};
+      const prevLink = document.getElementById('prevLink');
+      const nextLink = document.getElementById('nextLink');
+      if (prev) document.getElementById('prevTitle').textContent = prev.title || prev.url;
+      else { prevLink.style.visibility = 'hidden'; }
+      if (next) document.getElementById('nextTitle').textContent = next.title || next.url;
+      else { nextLink.style.visibility = 'hidden'; }
+      document.getElementById('navCenter').textContent = (current && current.group) ? current.group : 'Layer';
+    }
+
+    try { window.parent?.setMiddleActive?.(CURRENT); } catch {}
+    syncNavLabels();
+  </script>
+</body>
+</html>
+

+ 8 - 3
doc/module.md

@@ -241,9 +241,14 @@ Prev/Next 规则:
 > `Selection.layer()` / `Selection.unlayer()` + `xjs.fire()` / `xjs.layer()`
 
 - ✅ `examples/layer.html`(已有示例,但后续会补充“更像产品”的玩法)
-- ⬜ `layer/overview.html`(独立分组:弹窗 API + data-* 配置)
-- ⬜ `layer/test_selection_layer_dataset.html`
-- ⬜ `layer/test_static_fire.html`
+- ✅ `layer/list.html`(第二列:Layer 分组卡片导航)
+- ✅ `layer/overview.html`(独立分组:弹窗 API + 图标动画概览)
+- ✅ `layer/test_icons_svg_animation.html`(SVG icon:ring + mark 描边动画,体验接近 SweetAlert)
+- ✅ `layer/test_confirm_flow.html`(确认流程:warning → success/info 串联)
+- ✅ `layer/test_selection_layer_dataset.html`(Selection.layer + data-layer-*)
+- ✅ `layer/test_static_fire.html`(Layer.fire / Layer.$ builder)
+
+> 说明:Layer 的成功/失败等 icon 已切换为 **内联 SVG**,并使用 `animal.js` 的 `draw()` 技术(strokeDashoffset)做描边动画,同时用 spring easing 做弹性进入。
 
 ---
 

+ 193 - 105
layer.js

@@ -52,8 +52,8 @@
       display: flex;
       flex-direction: column;
       align-items: center;
-      transform: scale(0.9);
-      transition: transform 0.3s;
+      transform: scale(0.92);
+      transition: transform 0.26s ease;
       font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
     }
     .${PREFIX}overlay.show .${PREFIX}popup {
@@ -103,98 +103,41 @@
     .${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;
+      display: grid;
+      place-items: center;
     }
 
-    /* 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;
+    /* SVG Icon (SweetAlert-like, animated via xjs.draw + spring) */
+    .${PREFIX}icon svg {
+      width: 100%;
+      height: 100%;
       display: block;
-      top: 2.3em;
-      left: 1em;
-      border-radius: 0.15em;
-    }
-    .${PREFIX}error-line.left {
-      transform: rotate(45deg);
+      overflow: visible;
     }
-    .${PREFIX}error-line.right {
-      transform: rotate(-45deg);
+    .${PREFIX}svg-ring,
+    .${PREFIX}svg-mark {
+      fill: none;
+      stroke-linecap: round;
+      stroke-linejoin: round;
     }
-    
-    /* Warning Icon */
-    .${PREFIX}icon.warning {
-      border-color: #facea8;
-      color: #f8bb86;
+    .${PREFIX}svg-ring {
+      stroke-width: 4.5;
+      opacity: 0.95;
     }
-    .${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}svg-mark {
+      stroke-width: 6;
     }
-    .${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%;
+    .${PREFIX}svg-dot {
+      transform-box: fill-box;
+      transform-origin: center;
     }
+
+    .${PREFIX}icon.success { color: #a5dc86; }
+    .${PREFIX}icon.error { color: #f27474; }
+    .${PREFIX}icon.warning { color: #f8bb86; }
+    .${PREFIX}icon.info { color: #3fc3ee; }
   `;
 
   // Inject Styles
@@ -214,6 +157,7 @@
       this.promise = null;
       this.resolve = null;
       this.reject = null;
+      this._onKeydown = null;
     }
 
     // Constructor helper when called as function: const popup = Layer({...})
@@ -270,6 +214,9 @@
         confirmButtonColor: '#3085d6',
         cancelButtonColor: '#aaa',
         closeOnClickOutside: true,
+        closeOnEsc: true,
+        iconAnimation: true,
+        popupAnimation: true,
         ...options
       };
 
@@ -282,6 +229,14 @@
       return this.promise;
     }
 
+    static _getXjs() {
+      // Prefer xjs, fallback to animal (compat)
+      const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
+      if (!g) return null;
+      const x = g.xjs || g.animal;
+      return (typeof x === 'function') ? x : null;
+    }
+
     _render() {
       // Remove existing if any
       const existing = document.querySelector(`.${PREFIX}overlay`);
@@ -367,6 +322,7 @@
       // Animation
       requestAnimationFrame(() => {
         this.dom.overlay.classList.add('show');
+        this._didOpen();
       });
     }
 
@@ -374,35 +330,155 @@
       const icon = document.createElement('div');
       icon.className = `${PREFIX}icon ${type}`;
       
+      const svgNs = 'http://www.w3.org/2000/svg';
+      const svg = document.createElementNS(svgNs, 'svg');
+      svg.setAttribute('viewBox', '0 0 80 80');
+      svg.setAttribute('aria-hidden', 'true');
+      svg.setAttribute('focusable', 'false');
+
+      const ring = document.createElementNS(svgNs, 'circle');
+      ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
+      ring.setAttribute('cx', '40');
+      ring.setAttribute('cy', '40');
+      ring.setAttribute('r', '34');
+      ring.setAttribute('stroke', 'currentColor');
+
+      svg.appendChild(ring);
+
+      const addPath = (d, extraClass) => {
+        const p = document.createElementNS(svgNs, 'path');
+        p.setAttribute('d', d);
+        p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
+        p.setAttribute('stroke', 'currentColor');
+        svg.appendChild(p);
+        return p;
+      };
+
       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);
+        addPath('M23 41 L34.5 53 L58 29', `${PREFIX}svg-success`);
       } 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);
+        addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
+        addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
       } 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);
+        addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
+        const dot = document.createElementNS(svgNs, 'circle');
+        dot.setAttribute('class', `${PREFIX}svg-dot`);
+        dot.setAttribute('cx', '40');
+        dot.setAttribute('cy', '58');
+        dot.setAttribute('r', '3.2');
+        dot.setAttribute('fill', 'currentColor');
+        svg.appendChild(dot);
+      } else if (type === 'info') {
+        addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
+        const dot = document.createElementNS(svgNs, 'circle');
+        dot.setAttribute('class', `${PREFIX}svg-dot`);
+        dot.setAttribute('cx', '40');
+        dot.setAttribute('cy', '25');
+        dot.setAttribute('r', '3.2');
+        dot.setAttribute('fill', 'currentColor');
+        svg.appendChild(dot);
+      } else {
+        // Fallback to info-like ring only
       }
+
+      icon.appendChild(svg);
       
       return icon;
     }
 
+    _didOpen() {
+      // Keyboard close (ESC)
+      if (this.params.closeOnEsc) {
+        this._onKeydown = (e) => {
+          if (!e) return;
+          if (e.key === 'Escape') this._close(null);
+        };
+        document.addEventListener('keydown', this._onKeydown);
+      }
+
+      // Popup animation (optional)
+      if (this.params.popupAnimation) {
+        const X = Layer._getXjs();
+        if (X && this.dom.popup) {
+          // Override the CSS scale transition with a spring-ish entrance.
+          try {
+            this.dom.popup.style.transition = 'none';
+            this.dom.popup.style.transform = 'scale(0.92)';
+            X(this.dom.popup).animate({
+              scale: [0.92, 1],
+              y: [-6, 0],
+              duration: 520,
+              easing: { stiffness: 260, damping: 16 }
+            });
+          } catch {}
+        }
+      }
+
+      // Icon SVG draw animation
+      if (this.params.iconAnimation) {
+        this._animateIcon();
+      }
+
+      // User hook (SweetAlert-ish naming)
+      try {
+        if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
+      } catch {}
+    }
+
+    _animateIcon() {
+      const icon = this.dom.icon;
+      if (!icon) return;
+      const X = Layer._getXjs();
+      if (!X) return;
+
+      try {
+        X(icon).animate({
+          opacity: [0, 1],
+          scale: [0.68, 1],
+          duration: 520,
+          easing: { stiffness: 240, damping: 14 }
+        });
+      } catch {}
+
+      const svg = icon.querySelector('svg');
+      if (!svg) return;
+
+      const type = (this.params && this.params.icon) || '';
+      const ring = svg.querySelector(`.${PREFIX}svg-ring`);
+      const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
+      const dot = svg.querySelector(`.${PREFIX}svg-dot`);
+
+      // draw ring first
+      try { if (ring) X(ring).draw({ duration: 520, easing: 'ease-in-out', delay: 60 }); } catch {}
+
+      if (type === 'error') {
+        // two lines staggered
+        const left = marks[0];
+        const right = marks[1];
+        try { if (left) X(left).draw({ duration: 360, easing: 'ease-out', delay: 180 }); } catch {}
+        try { if (right) X(right).draw({ duration: 360, easing: 'ease-out', delay: 250 }); } catch {}
+        return;
+      }
+
+      // single mark (success/warning/info)
+      marks.forEach((m, i) => {
+        try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
+      });
+
+      if (dot) {
+        try {
+          dot.style.opacity = '0';
+          X(dot).animate({
+            opacity: [0, 1],
+            scale: [0.2, 1],
+            duration: 320,
+            delay: 280,
+            easing: { stiffness: 320, damping: 18 }
+          });
+        } catch {}
+      }
+    }
+
     _close(isConfirmed) {
       this.dom.overlay.classList.remove('show');
       setTimeout(() => {
@@ -411,6 +487,18 @@
         }
       }, 300);
 
+      if (this._onKeydown) {
+        try { document.removeEventListener('keydown', this._onKeydown); } catch {}
+        this._onKeydown = null;
+      }
+
+      try {
+        if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
+      } catch {}
+      try {
+        if (typeof this.params.didClose === 'function') this.params.didClose();
+      } catch {}
+
       if (isConfirmed === true) {
         this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
       } else if (isConfirmed === false) {

+ 216 - 122
xjs.js

@@ -68,8 +68,8 @@
       display: flex;
       flex-direction: column;
       align-items: center;
-      transform: scale(0.9);
-      transition: transform 0.3s;
+      transform: scale(0.92);
+      transition: transform 0.26s ease;
       font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
     }
     .${PREFIX}overlay.show .${PREFIX}popup {
@@ -119,98 +119,41 @@
     .${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;
+      display: grid;
+      place-items: center;
     }
 
-    /* Error Icon */
-    .${PREFIX}icon.error {
-      border-color: #f27474;
-      color: #f27474;
-    }
-    .${PREFIX}error-x-mark {
-      position: relative;
+    /* SVG Icon (SweetAlert-like, animated via xjs.draw + spring) */
+    .${PREFIX}icon svg {
+      width: 100%;
+      height: 100%;
       display: block;
+      overflow: visible;
     }
-    .${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}svg-ring,
+    .${PREFIX}svg-mark {
+      fill: none;
+      stroke-linecap: round;
+      stroke-linejoin: round;
     }
-    .${PREFIX}error-line.left {
-      transform: rotate(45deg);
+    .${PREFIX}svg-ring {
+      stroke-width: 4.5;
+      opacity: 0.95;
     }
-    .${PREFIX}error-line.right {
-      transform: rotate(-45deg);
+    .${PREFIX}svg-mark {
+      stroke-width: 6;
     }
-    
-    /* 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%;
+    .${PREFIX}svg-dot {
+      transform-box: fill-box;
+      transform-origin: center;
     }
+
+    .${PREFIX}icon.success { color: #a5dc86; }
+    .${PREFIX}icon.error { color: #f27474; }
+    .${PREFIX}icon.warning { color: #f8bb86; }
+    .${PREFIX}icon.info { color: #3fc3ee; }
   `;
 
   // Inject Styles
@@ -230,6 +173,7 @@
       this.promise = null;
       this.resolve = null;
       this.reject = null;
+      this._onKeydown = null;
     }
 
     // Constructor helper when called as function: const popup = Layer({...})
@@ -286,6 +230,9 @@
         confirmButtonColor: '#3085d6',
         cancelButtonColor: '#aaa',
         closeOnClickOutside: true,
+        closeOnEsc: true,
+        iconAnimation: true,
+        popupAnimation: true,
         ...options
       };
 
@@ -298,6 +245,14 @@
       return this.promise;
     }
 
+    static _getXjs() {
+      // Prefer xjs, fallback to animal (compat)
+      const g = (typeof window !== 'undefined') ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
+      if (!g) return null;
+      const x = g.xjs || g.animal;
+      return (typeof x === 'function') ? x : null;
+    }
+
     _render() {
       // Remove existing if any
       const existing = document.querySelector(`.${PREFIX}overlay`);
@@ -383,6 +338,7 @@
       // Animation
       requestAnimationFrame(() => {
         this.dom.overlay.classList.add('show');
+        this._didOpen();
       });
     }
 
@@ -390,35 +346,148 @@
       const icon = document.createElement('div');
       icon.className = `${PREFIX}icon ${type}`;
       
+      const svgNs = 'http://www.w3.org/2000/svg';
+      const svg = document.createElementNS(svgNs, 'svg');
+      svg.setAttribute('viewBox', '0 0 80 80');
+      svg.setAttribute('aria-hidden', 'true');
+      svg.setAttribute('focusable', 'false');
+
+      const ring = document.createElementNS(svgNs, 'circle');
+      ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
+      ring.setAttribute('cx', '40');
+      ring.setAttribute('cy', '40');
+      ring.setAttribute('r', '34');
+      ring.setAttribute('stroke', 'currentColor');
+      svg.appendChild(ring);
+
+      const addPath = (d, extraClass) => {
+        const p = document.createElementNS(svgNs, 'path');
+        p.setAttribute('d', d);
+        p.setAttribute('class', `${PREFIX}svg-mark ${PREFIX}svg-draw${extraClass ? ' ' + extraClass : ''}`);
+        p.setAttribute('stroke', 'currentColor');
+        svg.appendChild(p);
+        return p;
+      };
+
       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);
+        addPath('M23 41 L34.5 53 L58 29', `${PREFIX}svg-success`);
       } 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);
+        addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
+        addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
       } 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);
+        addPath('M40 20 L40 46', `${PREFIX}svg-warning-line`);
+        const dot = document.createElementNS(svgNs, 'circle');
+        dot.setAttribute('class', `${PREFIX}svg-dot`);
+        dot.setAttribute('cx', '40');
+        dot.setAttribute('cy', '58');
+        dot.setAttribute('r', '3.2');
+        dot.setAttribute('fill', 'currentColor');
+        svg.appendChild(dot);
+      } else if (type === 'info') {
+        addPath('M40 34 L40 56', `${PREFIX}svg-info-line`);
+        const dot = document.createElementNS(svgNs, 'circle');
+        dot.setAttribute('class', `${PREFIX}svg-dot`);
+        dot.setAttribute('cx', '40');
+        dot.setAttribute('cy', '25');
+        dot.setAttribute('r', '3.2');
+        dot.setAttribute('fill', 'currentColor');
+        svg.appendChild(dot);
+      } else {
+        // ring only
       }
+
+      icon.appendChild(svg);
       
       return icon;
     }
 
+    _didOpen() {
+      // Keyboard close (ESC)
+      if (this.params.closeOnEsc) {
+        this._onKeydown = (e) => {
+          if (!e) return;
+          if (e.key === 'Escape') this._close(null);
+        };
+        document.addEventListener('keydown', this._onKeydown);
+      }
+
+      // Popup animation (optional)
+      if (this.params.popupAnimation) {
+        const X = Layer._getXjs();
+        if (X && this.dom.popup) {
+          try {
+            this.dom.popup.style.transition = 'none';
+            this.dom.popup.style.transform = 'scale(0.92)';
+            X(this.dom.popup).animate({
+              scale: [0.92, 1],
+              y: [-6, 0],
+              duration: 520,
+              easing: { stiffness: 260, damping: 16 }
+            });
+          } catch {}
+        }
+      }
+
+      if (this.params.iconAnimation) {
+        this._animateIcon();
+      }
+
+      try {
+        if (typeof this.params.didOpen === 'function') this.params.didOpen(this.dom.popup);
+      } catch {}
+    }
+
+    _animateIcon() {
+      const icon = this.dom.icon;
+      if (!icon) return;
+      const X = Layer._getXjs();
+      if (!X) return;
+
+      try {
+        X(icon).animate({
+          opacity: [0, 1],
+          scale: [0.68, 1],
+          duration: 520,
+          easing: { stiffness: 240, damping: 14 }
+        });
+      } catch {}
+
+      const svg = icon.querySelector('svg');
+      if (!svg) return;
+
+      const type = (this.params && this.params.icon) || '';
+      const ring = svg.querySelector(`.${PREFIX}svg-ring`);
+      const marks = Array.from(svg.querySelectorAll(`.${PREFIX}svg-mark`));
+      const dot = svg.querySelector(`.${PREFIX}svg-dot`);
+
+      try { if (ring) X(ring).draw({ duration: 520, easing: 'ease-in-out', delay: 60 }); } catch {}
+
+      if (type === 'error') {
+        const left = marks[0];
+        const right = marks[1];
+        try { if (left) X(left).draw({ duration: 360, easing: 'ease-out', delay: 180 }); } catch {}
+        try { if (right) X(right).draw({ duration: 360, easing: 'ease-out', delay: 250 }); } catch {}
+        return;
+      }
+
+      marks.forEach((m, i) => {
+        try { X(m).draw({ duration: 420, easing: 'ease-out', delay: 180 + i * 60 }); } catch {}
+      });
+
+      if (dot) {
+        try {
+          dot.style.opacity = '0';
+          X(dot).animate({
+            opacity: [0, 1],
+            scale: [0.2, 1],
+            duration: 320,
+            delay: 280,
+            easing: { stiffness: 320, damping: 18 }
+          });
+        } catch {}
+      }
+    }
+
     _close(isConfirmed) {
       this.dom.overlay.classList.remove('show');
       setTimeout(() => {
@@ -427,6 +496,18 @@
         }
       }, 300);
 
+      if (this._onKeydown) {
+        try { document.removeEventListener('keydown', this._onKeydown); } catch {}
+        this._onKeydown = null;
+      }
+
+      try {
+        if (typeof this.params.willClose === 'function') this.params.willClose(this.dom.popup);
+      } catch {}
+      try {
+        if (typeof this.params.didClose === 'function') this.params.didClose();
+      } catch {}
+
       if (isConfirmed === true) {
         this.resolve({ isConfirmed: true, isDenied: false, isDismissed: false });
       } else if (isConfirmed === false) {
@@ -993,8 +1074,25 @@
         if (k in t.style) return t.style[k];
       }
       
-      // SVG attribute
+      // SVG
+      // For many SVG presentation attributes (e.g. strokeDashoffset), style overrides attribute.
+      // Prefer computed style / inline style when available, and fallback to attributes.
       if (isSVG(t)) {
+        // Geometry attrs must remain attrs (not style)
+        if (k === 'd' || k === 'points') {
+          const attr = toKebab(k);
+          if (t.hasAttribute(attr)) return t.getAttribute(attr);
+          if (t.hasAttribute(k)) return t.getAttribute(k);
+          return t.getAttribute(k);
+        }
+        try {
+          const cs = getComputedStyle(t);
+          const v = cs.getPropertyValue(toKebab(k));
+          if (v && v.trim()) return v.trim();
+        } catch (_) {}
+        try {
+          if (k in t.style) return t.style[k];
+        } catch (_) {}
         const attr = toKebab(k);
         if (t.hasAttribute(attr)) return t.getAttribute(attr);
         if (t.hasAttribute(k)) return t.getAttribute(k);
@@ -1012,21 +1110,6 @@
         return;
       }
       
-      // SVG attribute
-      if (isSVG(t)) {
-        const attr = toKebab(k);
-        t.setAttribute(attr, v);
-        // Verify write
-        // if (k === 'd' && Math.random() < 0.01) console.log('Readback:', t.getAttribute('d')?.slice(0, 10));
-        return;
-      }
-      
-      // Fallback
-      if ((k === 'd' || k === 'points') && isEl(t)) {
-         t.setAttribute(k, v);
-         return;
-      }
-
       // scroll
       if (k === 'scrollTop' || k === 'scrollLeft') {
         t[k] = v;
@@ -1045,8 +1128,19 @@
         return;
       }
       
-      // SVG attribute
+      // SVG
+      // Prefer style for presentation attrs so it can animate when svgDraw initialized styles.
       if (isSVG(t)) {
+        if (k === 'd' || k === 'points') {
+          t.setAttribute(k, v);
+          return;
+        }
+        try {
+          if (k in t.style) {
+            t.style[k] = v;
+            return;
+          }
+        } catch (_) {}
         const attr = toKebab(k);
         t.setAttribute(attr, v);
         return;