robert преди 17 часа
родител
ревизия
79b610185f

+ 4 - 4
Guide.md

@@ -262,14 +262,14 @@ go run main.go build path/to/xjs.js
 ### 11.1 入口与用法
 
 - **主入口**:`$`(同时保留 `xjs/animal` 兼容别名)
-- **直接触发**:`$.layer(options)` / `Layer.run(options)` / `Layer(options)`
+- **直接触发**:`Layer.run(options)` / `Layer(options)`
 - **链式构建**:`Layer.$(baseOptions).step(...).run()`
 - **快捷 steps**:`Layer.flow(steps, baseOptions)`
 
 示例(最简弹窗):
 
 ```js
-$.layer({
+Layer.run({
   title: 'Hello',
   text: 'Triggered by click',
   showCancelButton: true
@@ -310,10 +310,10 @@ $.layer({
 
 ```js
 // 直接移动 DOM(默认)
-$.layer({ title: 'Edit', dom: '#my_form', showCancelButton: true });
+Layer.run({ title: 'Edit', dom: '#my_form', showCancelButton: true });
 
 // 以 clone 方式展示
-$.layer({ title: 'Preview', dom: '#my_form', domMode: 'clone' });
+Layer.run({ title: 'Preview', dom: '#my_form', domMode: 'clone' });
 ```
 
 ### 11.5 Steps / Flow

+ 8 - 8
README.md

@@ -152,22 +152,22 @@ tl.add('.box-1', { x: 100 })
 
 虽然推荐使用 `xjs('.el').layer(...)` 绑定事件,但你也完全可以通过全局对象手动调用。
 
-#### 基础弹窗 (直接调用 xjs.layer)
+#### 基础弹窗 (直接调用 Layer.run)
 ```javascript
-xjs.layer({
+Layer.run({
   title: 'Hello World',
-  text: 'This is a message from xjs.layer()'
+  text: 'This is a message from Layer.run()'
 });
 ```
 
 #### Confirm 确认流程
 ```javascript
-xjs.layer({
+Layer.run({
   title: '提交表单?',
   showCancelButton: true
 }).then((res) => {
   if (res.isConfirmed) {
-    xjs.layer({ title: '提交成功!', icon: 'success' });
+    Layer.run({ title: '提交成功!', icon: 'success' });
   }
 });
 ```
@@ -175,13 +175,13 @@ xjs.layer({
 #### 弹窗内容支持 DOM/HTML
 ```javascript
 // 传入 DOM 元素
-xjs.layer({
+Layer.run({
   title: 'Custom Content',
   content: document.querySelector('#my-template')
 });
 
 // 或者传入 HTML 字符串
-xjs.layer({
+Layer.run({
   html: '<strong>Bold Text</strong><br>New line'
 });
 ```
@@ -212,4 +212,4 @@ xjs.layer({
 | **`xjs.spring(config)`** | 创建物理缓动函数 |
 | **`xjs.timeline()`** | 创建时间轴 |
 | **`xjs.scroll(controls)`** | 绑定滚动驱动 |
-| **`xjs.layer(options)`** | **直接弹出窗口 (别名 xjs.fire)** |
+| **`Layer.run(options)`** | **直接弹出窗口 (返回 Promise)** |

+ 5 - 5
doc/examples/layer.html

@@ -97,7 +97,7 @@ xjs('.btn-data').layer(); // reads data-layer-* attributes
 
 // 3) Fire directly (manual usage)
 document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
-  xjs.layer({ title: 'Hello from xjs.layer()', icon: 'success' });
+  Layer.run({ title: 'Hello from Layer.run()', icon: 'success' });
 });</pre>
 
       <pre id="html-code" class="html-view">&lt;button class="btn danger btn-delete"&gt;Delete (bind .layer)&lt;/button&gt;
@@ -109,7 +109,7 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
   data-layer-icon="success"
 &gt;Open (data-layer-*)&lt;/button&gt;
 
-&lt;button class="btn btn-direct"&gt;Open (xjs.layer)&lt;/button&gt;</pre>
+&lt;button class="btn btn-direct"&gt;Open (Layer.run)&lt;/button&gt;</pre>
 
       <pre id="css-code" class="css-view">.demo-visual {
   gap: 14px;
@@ -156,7 +156,7 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
 }</pre>
 
       <div class="feature-desc">
-        <strong>功能说明:</strong>演示 <code class="inline">Selection.layer()</code> 的三种用法:选择器绑定弹窗、data-* 配置、以及直接调用 <code class="inline">xjs.layer()</code>。
+        <strong>功能说明:</strong>演示 <code class="inline">Selection.layer()</code> 的三种用法:选择器绑定弹窗、data-* 配置、以及直接调用 <code class="inline">Layer.run()</code>。
       </div>
 
       <div class="demo-visual">
@@ -170,7 +170,7 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
             data-layer-icon="success"
           >Open (data-layer-*)</button>
 
-          <button class="btn btn-direct">Open (xjs.layer)</button>
+          <button class="btn btn-direct">Open (Layer.run)</button>
         </div>
         <div class="hint">
           Make sure you included both <code class="inline">animal.js</code> and <code class="inline">layer.js</code>.
@@ -226,7 +226,7 @@ document.querySelector('.btn-direct').addEventListener('click', () =&gt; {
       const direct = document.querySelector('.btn-direct');
       if (direct) {
         direct.onclick = () => {
-          xjs.layer({ title: 'Hello from xjs.layer()', text: 'Direct call (alias of xjs.fire)', icon: 'success' });
+          Layer.run({ title: 'Hello from Layer.run()', text: 'Direct call', icon: 'success' });
         };
       }
     }

+ 1 - 1
doc/layer/list.html

@@ -101,7 +101,7 @@
 
       <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">$.layer(...).then(res =&gt; ...)</div>
+          <div class="preview-pill">Layer.run(...).then(res =&gt; ...)</div>
         </div>
         <div class="card-footer">Confirm flow (success / error)</div>
       </div>

+ 6 - 6
doc/layer/overview.html

@@ -73,7 +73,7 @@ xjs('.btn.confirm').forEach((el) =&gt; {
   // Idempotent binding (in case this demo re-inits)
   if (el._xjsConfirmHandler) el.removeEventListener('click', el._xjsConfirmHandler);
   el._xjsConfirmHandler = () =&gt; {
-    xjs.layer({
+    Layer.run({
       title: 'Delete item?',
       text: 'This action cannot be undone.',
       icon: 'question',
@@ -82,9 +82,9 @@ xjs('.btn.confirm').forEach((el) =&gt; {
       cancelButtonText: 'Cancel'
     }).then((res) =&gt; {
       if (res.isConfirmed) {
-        xjs.layer({ title: 'Deleted', text: 'Done', icon: 'success' });
+        Layer.run({ title: 'Deleted', text: 'Done', icon: 'success' });
       } else if (res.dismiss === 'cancel') {
-        xjs.layer({ title: 'Cancelled', text: 'Nothing happened', icon: 'info' });
+        Layer.run({ title: 'Cancelled', text: 'Nothing happened', icon: 'info' });
       }
     });
   };
@@ -182,7 +182,7 @@ xjs('.btn.confirm').forEach((el) =&gt; {
         xjs('.btn.confirm').forEach((el) => {
           if (el._xjsConfirmHandler) el.removeEventListener('click', el._xjsConfirmHandler);
           el._xjsConfirmHandler = () => {
-            xjs.layer({
+            Layer.run({
               title: 'Delete item?',
               text: 'This action cannot be undone.',
               icon: 'question',
@@ -191,9 +191,9 @@ xjs('.btn.confirm').forEach((el) =&gt; {
               cancelButtonText: 'Cancel'
             }).then((res) => {
               if (res.isConfirmed) {
-                xjs.layer({ title: 'Deleted', text: 'Done', icon: 'success' });
+                Layer.run({ title: 'Deleted', text: 'Done', icon: 'success' });
               } else if (res.dismiss === 'cancel') {
-                xjs.layer({ title: 'Cancelled', text: 'Nothing happened', icon: 'info' });
+                Layer.run({ title: 'Cancelled', text: 'Nothing happened', icon: 'info' });
               }
             });
           };

+ 8 - 8
doc/layer/test_confirm_flow.html

@@ -63,7 +63,7 @@
         </div>
       </div>
 
-      <pre id="js-code" class="code-view active">xjs.layer({
+      <pre id="js-code" class="code-view active">Layer.run({
   title: 'Delete item?',
   text: 'This action cannot be undone.',
   icon: 'warning',
@@ -72,9 +72,9 @@
   cancelButtonText: 'Cancel'
 }).then((res) => {
   if (res.isConfirmed) {
-    xjs.layer({ title: 'Deleted', icon: 'success' });
+    Layer.run({ title: 'Deleted', icon: 'success' });
   } else {
-    xjs.layer({ title: 'Cancelled', icon: 'info' });
+    Layer.run({ title: 'Cancelled', icon: 'info' });
   }
 });</pre>
 
@@ -128,7 +128,7 @@
     }
 
     function openConfirm() {
-      xjs.layer({
+      Layer.run({
         title: 'Delete item?',
         text: 'This action cannot be undone.',
         icon: 'question',
@@ -138,17 +138,17 @@
         cancelButtonText: 'Cancel'
       }).then((res) => {
         if (res.isConfirmed) {
-          xjs.layer({ title: 'Deleted', text: 'Done', icon: 'success' });
+          Layer.run({ title: 'Deleted', text: 'Done', icon: 'success' });
         } else if (res.dismiss === 'cancel') {
-          xjs.layer({ title: 'Cancelled', text: 'No changes', icon: 'info' });
+          Layer.run({ title: 'Cancelled', text: 'No changes', icon: 'info' });
         } else {
-          xjs.layer({ title: 'Dismissed', text: 'Backdrop / ESC', icon: 'info' });
+          Layer.run({ title: 'Dismissed', text: 'Backdrop / ESC', icon: 'info' });
         }
       });
     }
 
     function openError() {
-      xjs.layer({ title: 'Error', text: 'Example error popup', icon: 'error', popupAnimation: true });
+      Layer.run({ title: 'Error', text: 'Example error popup', icon: 'error', popupAnimation: true });
     }
 
     function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }

+ 3 - 3
doc/layer/test_custom_animation.html

@@ -103,7 +103,7 @@
 
   const titleCap = name ? (name[0].toUpperCase() + name.slice(1)) : '';
 
-  $.layer({
+  Layer.run({
     title: titleCap,
     text: 'Lottie animation icon',
     icon: 'svg:' + name,
@@ -234,7 +234,7 @@
       const iconBox = name === 'loadding' ? '11em' : null;
 
       // Keep the displayed code consistent with what is executed.
-      const code = `$.layer({
+      const code = `Layer.run({
   title: '${titleCap}',
   text: 'Lottie animation icon',
   icon: 'svg:${name}',
@@ -252,7 +252,7 @@
         try { if (window.__highlightCssViews) window.__highlightCssViews(); } catch {}
       }
 
-      $.layer({
+      Layer.run({
         title: titleCap,
         text: 'Lottie animation icon',
         icon: 'svg:' + name,

+ 6 - 6
doc/layer/test_custom_animation_background.html

@@ -119,7 +119,7 @@ function openSingle(type, sizeKey) {
   const closeOnEsc = !!document.getElementById('optEsc')?.checked;
   const size = SIZE_PRESETS[sizeKey] || null;
 
-  $.layer({
+  Layer.run({
     title: 'Background: ' + type.toUpperCase(),
     text: 'Layer popup background demo.',
     background: BACKGROUNDS[type],
@@ -141,7 +141,7 @@ function openSteps(mode) {
   const a = BACKGROUNDS.svg;
   const b = BACKGROUNDS.image;
 
-  $.layer({
+  Layer.run({
     title: 'Step 1',
     text: 'Background should follow when different.',
     background: mode === 'same' ? same : a,
@@ -304,7 +304,7 @@ function openSteps(mode) {
       const closeOnEsc = !!document.getElementById('optEsc')?.checked;
       const size = SIZE_PRESETS[sizeKey] || null;
 
-      const code = `$.layer({
+      const code = `Layer.run({
   title: 'Background: ${type.toUpperCase()}',
   text: 'Layer popup background demo.',
   background: '${BACKGROUNDS[type]}',
@@ -317,7 +317,7 @@ function openSteps(mode) {
 });`;
       setJsCode(code);
 
-      $.layer({
+      Layer.run({
         title: 'Background: ' + type.toUpperCase(),
         text: 'Layer popup background demo.',
         background: BACKGROUNDS[type],
@@ -340,7 +340,7 @@ function openSteps(mode) {
       const a = BACKGROUNDS.svg;
       const b = BACKGROUNDS.image;
 
-      const code = `$.layer({
+      const code = `Layer.run({
   title: 'Step 1',
   text: 'Background should follow when different.',
   background: ${mode === 'same' ? 'same' : 'a'},
@@ -367,7 +367,7 @@ function openSteps(mode) {
         return `'${b}'`;
       }));
 
-      $.layer({
+      Layer.run({
         title: 'Step 1',
         text: 'Background should follow when different.',
         background: mode === 'same' ? same : a,

+ 3 - 3
doc/layer/test_custom_animation_ballsorting.html

@@ -94,7 +94,7 @@
 
   const titleCap = name ? (name[0].toUpperCase() + name.slice(1)) : '';
 
-  $.layer({
+  Layer.run({
     title: titleCap,
     text: 'Lottie animation icon',
     icon: 'svg:' + name,
@@ -215,7 +215,7 @@
       const titleCap = name ? (name[0].toUpperCase() + name.slice(1)) : '';
 
       // Keep the displayed code consistent with what is executed.
-      const code = `$.layer({
+      const code = `Layer.run({
   title: '${titleCap}',
   text: 'Lottie animation icon',
   icon: 'svg:${name}',
@@ -226,7 +226,7 @@
       const jsPre = document.getElementById('js-code');
       if (jsPre) jsPre.textContent = code;
 
-      $.layer({
+      Layer.run({
         title: titleCap,
         text: 'Lottie animation icon',
         icon: 'svg:' + name,

+ 3 - 3
doc/layer/test_custom_animation_banner.html

@@ -94,7 +94,7 @@
 
   const titleCap = name ? (name[0].toUpperCase() + name.slice(1)) : '';
 
-  $.layer({
+  Layer.run({
     title: titleCap,
     text: 'Lottie animation icon',
     icon: 'svg:' + name,
@@ -215,7 +215,7 @@
       const titleCap = name ? (name[0].toUpperCase() + name.slice(1)) : '';
 
       // Keep the displayed code consistent with what is executed.
-      const code = `$.layer({
+      const code = `Layer.run({
   title: '${titleCap}',
   text: 'Lottie animation icon',
   icon: 'svg:${name}',
@@ -226,7 +226,7 @@
       const jsPre = document.getElementById('js-code');
       if (jsPre) jsPre.textContent = code;
 
-      $.layer({
+      Layer.run({
         title: titleCap,
         text: 'Lottie animation icon',
         icon: 'svg:' + name,

+ 11 - 11
doc/layer/test_dom_steps.html

@@ -105,7 +105,7 @@
 
       <pre id="js-code" class="code-view active">// 1) Single popup: show a hidden DOM block inside Layer
 $('#openDom').click(function () {
-  $.layer({
+  Layer.run({
     title: 'DOM content',
     dom: '#demo_dom_block',
     showCancelButton: true,
@@ -116,7 +116,7 @@ $('#openDom').click(function () {
 
 // 2) Step flow (wizard) - no icon during steps; final summary is allowed
 $('#openWizard').click(function () {
-  $.layer({
+  Layer.run({
     closeOnClickOutside: false,
     closeOnEsc: false,
     popupAnimation: false
@@ -155,7 +155,7 @@ $('#openWizard').click(function () {
   .run()
   .then((res) =&gt; {
     if (res.isConfirmed) {
-      $.layer({
+      Layer.run({
         title: 'Done',
         icon: 'success',
         popupAnimation: false,
@@ -168,14 +168,14 @@ $('#openWizard').click(function () {
 
 // 3) Step container shorthand (auto steps + form data)
 $('#openStepContainer').click(function () {
-  $.layer({
+  Layer.run({
     step: '#step_container',
     stepItem: '.stepItem',
     title: 'Create task',
     showCancelButton: true
   }).then((res) =&gt; {
     if (res.isConfirmed) {
-      $.layer({
+      Layer.run({
         title: 'Collected data',
         icon: 'success',
         popupAnimation: false,
@@ -381,7 +381,7 @@ then restore it back (and restore display/hidden state) on close. */</pre>
     function bindDemoHandlers() {
       if (typeof $ !== 'function') return;
       $('#openDom').click(function () {
-        $.layer({
+        Layer.run({
           title: 'DOM content',
           dom: '#demo_dom_block',
           popupAnimation: true,
@@ -392,7 +392,7 @@ then restore it back (and restore display/hidden state) on close. */</pre>
       });
 
       $('#openWizard').click(function () {
-        $.layer({
+        Layer.run({
           closeOnClickOutside: false,
           closeOnEsc: false,
           popupAnimation: false
@@ -431,7 +431,7 @@ then restore it back (and restore display/hidden state) on close. */</pre>
       .run()
         .then((res) => {
           if (res.isConfirmed) {
-            $.layer({
+            Layer.run({
               title: 'Done',
               icon: 'success',
               popupAnimation: false,
@@ -439,20 +439,20 @@ then restore it back (and restore display/hidden state) on close. */</pre>
               html: '<pre>' + JSON.stringify(res.value, null, 2) + '</pre>'
             });
           } else {
-            $.layer({ title: 'Dismissed', icon: 'info', popupAnimation: false, replace: true, text: 'Flow cancelled' });
+            Layer.run({ title: 'Dismissed', icon: 'info', popupAnimation: false, replace: true, text: 'Flow cancelled' });
           }
         });
       });
 
       $('#openStepContainer').click(function () {
-        $.layer({
+        Layer.run({
           step: '#step_container',
           stepItem: '.stepItem',
           title: 'Create task',
           showCancelButton: true
         }).then((res) => {
           if (res.isConfirmed) {
-            $.layer({
+            Layer.run({
               title: 'Collected data',
               icon: 'success',
               popupAnimation: false,

+ 3 - 3
doc/layer/test_icons_svg_animation.html

@@ -103,7 +103,7 @@
   const title = String(type || '');
   const titleCap = title ? (title[0].toUpperCase() + title.slice(1)) : '';
 
-  $.layer({
+  Layer.run({
     title: titleCap,
     text: 'SVG draw + spring entrance',
     icon: title,
@@ -290,7 +290,7 @@
       const titleCap = title ? (title[0].toUpperCase() + title.slice(1)) : '';
 
       // Keep the displayed code consistent with what is executed.
-      const code = `$.layer({
+      const code = `Layer.run({
   title: '${titleCap}',
   text: 'SVG draw + spring entrance',
   icon: '${title}',
@@ -302,7 +302,7 @@
       if (jsPre) jsPre.textContent = code;
 
       try {
-        $.layer({
+        Layer.run({
           title: titleCap,
           text: 'SVG draw + spring entrance',
           icon: title,

+ 1 - 1
doc/layer/test_static_fire.html

@@ -141,7 +141,7 @@ Layer.$()
         .run()
         .then((res) => {
           console.log('[Layer.builder] result:', res);
-          if (res.isConfirmed) $.layer({ title: 'Confirmed', icon: 'success', popupAnimation: true });
+          if (res.isConfirmed) Layer.run({ title: 'Confirmed', icon: 'success', popupAnimation: true });
         });
     }
 

+ 1 - 1
doc/module.md

@@ -238,7 +238,7 @@ Prev/Next 规则:
 
 ### I. Layer 集成(联动展示库特性)
 
-> `Selection.layer()` / `Selection.unlayer()` + `$.run()` / `$.layer()`
+> `Selection.layer()` / `Selection.unlayer()` + `Layer.run()` / `Layer.$().run()`
 
 - ✅ `examples/layer.html`(已有示例,但后续会补充“更像产品”的玩法)
 - ✅ `layer/list.html`(第二列:Layer 分组卡片导航)

+ 87 - 102
layer.js

@@ -460,6 +460,77 @@
       return true;
     }
 
+    _clearPopup() {
+      const popup = this.dom && this.dom.popup;
+      if (!popup) return null;
+      while (popup.firstChild) popup.removeChild(popup.firstChild);
+      return popup;
+    }
+
+    _bindOverlayClick(handler) {
+      const overlay = this.dom && this.dom.overlay;
+      if (!overlay) return;
+      try {
+        if (overlay._layerOnClick) {
+          overlay.removeEventListener('click', overlay._layerOnClick);
+        }
+      } catch {}
+      overlay._layerOnClick = null;
+      if (!handler) return;
+      overlay._layerOnClick = handler;
+      overlay.addEventListener('click', handler);
+    }
+
+    _mountOverlay() {
+      const overlay = this.dom && this.dom.overlay;
+      if (!overlay) return;
+      // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
+      // then show on a clean frame so opacity transitions are smooth.
+      const ready = this._cssReady && typeof this._cssReady.then === 'function' ? this._cssReady : Promise.resolve();
+      ready.then(() => {
+        try { overlay.style.visibility = ''; } catch {}
+        if (!overlay.parentNode) {
+          document.body.appendChild(overlay);
+        }
+        // Double-rAF gives the browser a chance to apply styles before animating.
+        requestAnimationFrame(() => {
+          requestAnimationFrame(() => {
+            overlay.classList.add('show');
+            this._didOpen();
+          });
+        });
+      });
+    }
+
+    _applyPopupBasics(options, { forceBackground = false } = {}) {
+      this._applyPopupSize(options);
+      this._applyPopupTheme(options);
+      this._applyBackgroundForOptions(options, { force: forceBackground });
+    }
+
+    _buildActions({ flow = false } = {}) {
+      this.dom.actions = el('div', `${PREFIX}actions`);
+      this.dom.popup.appendChild(this.dom.actions);
+      if (flow) {
+        this._updateFlowActions();
+        return;
+      }
+
+      // Cancel Button
+      if (this.params.showCancelButton) {
+        this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
+        this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText);
+        this.dom.cancelBtn.onclick = () => this._handleCancel();
+        this.dom.actions.appendChild(this.dom.cancelBtn);
+      }
+
+      // Confirm Button
+      this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
+      this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText);
+      this.dom.confirmBtn.onclick = () => this._handleConfirm();
+      this.dom.actions.appendChild(this.dom.confirmBtn);
+    }
+
     _render(meta = null) {
       this._destroySvgAniIcon();
       // Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
@@ -515,11 +586,7 @@
       // Create Popup
       this.dom.popup = el('div', `${PREFIX}popup`);
       this.dom.overlay.appendChild(this.dom.popup);
-      this._applyPopupSize(this.params);
-      this._applyPopupTheme(this.params);
-
-      // Background (full popup)
-      this._applyBackgroundForOptions(this.params, { force: true });
+      this._applyPopupBasics(this.params, { forceBackground: true });
 
       // Flow mode: pre-mount all steps and switch by hide/show (DOM continuity, less jitter)
       if (meta && meta.flow) {
@@ -557,63 +624,18 @@
       }
 
       // Actions
-      this.dom.actions = el('div', `${PREFIX}actions`);
-      
-      // Cancel Button
-      if (this.params.showCancelButton) {
-        this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, '');
-        this._setButtonContent(this.dom.cancelBtn, this.params.cancelButtonText);
-        this.dom.cancelBtn.onclick = () => this._handleCancel();
-        this.dom.actions.appendChild(this.dom.cancelBtn);
-      }
-
-      // Confirm Button
-      this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, '');
-      this._setButtonContent(this.dom.confirmBtn, this.params.confirmButtonText);
-      this.dom.confirmBtn.onclick = () => this._handleConfirm();
-      this.dom.actions.appendChild(this.dom.confirmBtn);
-
-      this.dom.popup.appendChild(this.dom.actions);
+      this._buildActions();
 
       // Event Listeners
       if (this.params.closeOnClickOutside) {
-        const onClick = (e) => {
-          if (e.target === this.dom.overlay) {
-            this._close(null); // Dismiss
-          }
-        };
-        try {
-          if (this.dom.overlay._layerOnClick) {
-            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
-          }
-        } catch {}
-        this.dom.overlay._layerOnClick = onClick;
-        this.dom.overlay.addEventListener('click', onClick);
+        this._bindOverlayClick((e) => {
+          if (e.target === this.dom.overlay) this._close(null); // Dismiss
+        });
       } else {
-        try {
-          if (this.dom.overlay._layerOnClick) {
-            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
-            this.dom.overlay._layerOnClick = null;
-          }
-        } catch {}
+        this._bindOverlayClick(null);
       }
 
-      // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
-      // then show on a clean frame so opacity transitions are smooth.
-      const ready = this._cssReady && typeof this._cssReady.then === 'function' ? this._cssReady : Promise.resolve();
-      ready.then(() => {
-        try { this.dom.overlay.style.visibility = ''; } catch {}
-        if (!this.dom.overlay.parentNode) {
-          document.body.appendChild(this.dom.overlay);
-        }
-        // Double-rAF gives the browser a chance to apply styles before animating.
-        requestAnimationFrame(() => {
-          requestAnimationFrame(() => {
-            this.dom.overlay.classList.add('show');
-            this._didOpen();
-          });
-        });
-      });
+      this._mountOverlay();
     }
 
     _renderFlowUI() {
@@ -623,13 +645,8 @@
       if (!popup) return;
 
       // Clear popup
-      while (popup.firstChild) popup.removeChild(popup.firstChild);
-
-      this._applyPopupSize(this.params);
-      this._applyPopupTheme(this.params);
-
-      // Background (full popup)
-      this._applyBackgroundForOptions(this.params, { force: true });
+      this._clearPopup();
+      this._applyPopupBasics(this.params, { forceBackground: true });
 
       // Icon (in flow: suppressed by default unless question or summary)
       this.dom.icon = null;
@@ -676,48 +693,18 @@
       popup.appendChild(this.dom.content);
 
       // Actions
-      this.dom.actions = el('div', `${PREFIX}actions`);
-      popup.appendChild(this.dom.actions);
-      this._updateFlowActions();
+      this._buildActions({ flow: true });
 
       // Event Listeners
       if (this.params.closeOnClickOutside) {
-        const onClick = (e) => {
-          if (e.target === this.dom.overlay) {
-            this._close(null, 'backdrop');
-          }
-        };
-        try {
-          if (this.dom.overlay._layerOnClick) {
-            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
-          }
-        } catch {}
-        this.dom.overlay._layerOnClick = onClick;
-        this.dom.overlay.addEventListener('click', onClick);
+        this._bindOverlayClick((e) => {
+          if (e.target === this.dom.overlay) this._close(null, 'backdrop');
+        });
       } else {
-        try {
-          if (this.dom.overlay._layerOnClick) {
-            this.dom.overlay.removeEventListener('click', this.dom.overlay._layerOnClick);
-            this.dom.overlay._layerOnClick = null;
-          }
-        } catch {}
+        this._bindOverlayClick(null);
       }
 
-      // Avoid first-open overlay "flash": wait for CSS before inserting into DOM,
-      // then show on a clean frame so opacity transitions are smooth.
-      const ready = this._cssReady && typeof this._cssReady.then === 'function' ? this._cssReady : Promise.resolve();
-      ready.then(() => {
-        try { this.dom.overlay.style.visibility = ''; } catch {}
-        if (!this.dom.overlay.parentNode) {
-          document.body.appendChild(this.dom.overlay);
-        }
-        requestAnimationFrame(() => {
-          requestAnimationFrame(() => {
-            this.dom.overlay.classList.add('show');
-            this._didOpen();
-          });
-        });
-      });
+      this._mountOverlay();
     }
 
     _updateFlowActions() {
@@ -2141,11 +2128,9 @@
     _rerenderInside() {
       this._destroySvgAniIcon();
       // Update popup content without recreating overlay (used in flow transitions)
-      const popup = this.dom && this.dom.popup;
+      const popup = this._clearPopup();
       if (!popup) return;
 
-      // Clear popup (but keep reference)
-      while (popup.firstChild) popup.removeChild(popup.firstChild);
       this._applyPopupTheme(this.params);
 
       // Icon