Răsfoiți Sursa

fix bug,去掉lottlie标签和css

robert 19 ore în urmă
părinte
comite
a22c1fb18a
5 a modificat fișierele cu 257 adăugiri și 267 ștergeri
  1. 56 171
      Guide.md
  2. 17 4
      doc/layer/test_custom_animation.html
  3. 6 0
      doc/layer/test_custom_animation_background.html
  4. 135 81
      layer.js
  5. 43 11
      xjs.css

+ 56 - 171
Guide.md

@@ -257,196 +257,81 @@ go run main.go build path/to/xjs.js
 
 ---
 
-## 11. Layer 新增能力:DOM 内容 / Steps(分步弹窗
+## 11. Layer 使用与文档(最新
 
-### 11.0 最新使用入口(推荐)
+### 11.1 入口与用法
 
 - **主入口**:`$`(同时保留 `xjs/animal` 兼容别名)
-- **静态弹窗**:`Layer.run(options)` 或 `$.run(options)`
-- **链式弹窗**:`$.layer(options).step(...).run()`
-- **事件绑定**:`$('#btn').click(function () { /* this 为按钮 */ })`
+- **直接触发**:`$.layer(options)` / `Layer.run(options)` / `Layer(options)`
+- **链式构建**:`Layer.$(baseOptions).step(...).run()`
+- **快捷 steps**:`Layer.flow(steps, baseOptions)`
 
-示例(点击触发):
+示例(最简弹窗):
 
 ```js
-$('#openDialog').click(function () {
-  $.layer({
-    title: 'Hello',
-    text: 'Triggered by click',
-    showCancelButton: true
-  });
+$.layer({
+  title: 'Hello',
+  text: 'Triggered by click',
+  showCancelButton: true
 });
 ```
 
-### 11.1 DOM 内容(显示指定 DOM,而不是一句话)
-
-Layer 现在支持把“页面里已有的 DOM(可默认隐藏)”挂载到弹窗内容区域展示:
-
-- **推荐参数**:`dom: '#selector' | Element | <template>`
-- **兼容参数**:`content: '#selector' | Element`(历史用法仍可用)
-- **行为**:
-  - 默认模式为 **move**:把 DOM 节点移动进弹窗,关闭时会 **自动还原到原位置**(同时还原 `hidden` / `style.display`)
-  - 可选 **clone**:通过 `domMode:'clone'` 或 `content:{ dom:'#el', clone:true }` 克隆节点展示(不移动原节点)
-
-示例:
+### 11.2 常用参数(单弹窗)
+
+- **内容**:
+  - `title` / `text`(纯文本)
+  - `html`(innerHTML)
+  - `dom`(推荐):`'#selector' | Element | <template>`
+  - `content`(兼容):`'#selector' | Element | { dom, selector, mode, clone }`
+  - `domMode`:`'move'`(默认,关闭时还原)/ `'clone'`(克隆展示)
+- **按钮**:`confirmButtonText` / `cancelButtonText` / `showCancelButton`
+- **关闭行为**:`closeOnClickOutside` / `closeOnEsc`
+- **动画**:`popupAnimation`(弹窗进入)/ `iconAnimation`(图标动效)
+- **图标**:`icon`(内置类型或 `svg:Name`)/ `iconSize` / `iconLoop`(Lottie)
+- **背景**:`background: 'svg(...)' | 'url(...)' | 'css(...)'`
+- **尺寸**:`width` / `height`(数字默认 px;字符串任意单位)
+  - `iwidth` / `iheight`:图标容器 `.layer-icon` 尺寸(可覆盖 CSS 默认值)
+- **主题**:`theme: 'auto' | 'light' | 'dark'`
+  - `auto` 跟随 `prefers-color-scheme`(暗色环境自动用浅色文字)
+  - `light` 强制浅色文字(适合深色背景)
+  - `dark` 强制深色文字(适合浅色背景)
+- **覆盖**:`replace: true`(替换当前已存在弹窗)
+
+### 11.3 Hooks 与返回值
+
+- `preConfirm(popup)`:返回 `false` 阻止关闭/下一步;可返回值或 Promise。
+- `didOpen(popup)` / `willClose(popup)` / `didClose()`:生命周期回调。
+- Promise resolve 结构:
+  - `isConfirmed` / `isDismissed`
+  - `value`:非 steps 为单值;steps 为数组
+  - `data`:steps 容器模式下自动汇总表单数据
+
+### 11.4 DOM 内容
 
 ```js
-// 页面中一个默认隐藏的 DOM
-// <div id="my_form" style="display:none">...</div>
-
-$('#openForm').click(function () {
-  $.layer({
-    title: 'Edit profile',
-    dom: '#my_form',
-    showCancelButton: true,
-    confirmButtonText: 'Save'
-  });
-});
-```
+// 直接移动 DOM(默认)
+$.layer({ title: 'Edit', dom: '#my_form', showCancelButton: true });
 
-### 11.2 Steps(多个对话框 next 平滑转化 / 分步填写)
+// 以 clone 方式展示
+$.layer({ title: 'Preview', dom: '#my_form', domMode: 'clone' });
+```
 
-Layer 支持“同一弹窗内”的步骤流:点击确认按钮不关闭,而是平滑切换到下一步;最后一步确认后 resolve。
+### 11.5 Steps / Flow
 
-演示:`/#layer_test__dom__steps`
+- 链式:`Layer.$(base).step(step1).step(step2).run()`
+- 快捷:`Layer.flow([step1, step2], base)`
+- 容器式:`{ step: '#container', stepItem: '.item' }`
+- Steps 按钮图标:`nextIcon` / `prevIcon`
 
-推荐写法(链式):
+示例(链式 steps):
 
 ```js
-$('#openWizard').click(function () {
-  $.layer({
-    icon: 'info',
-    closeOnClickOutside: false,
-    closeOnEsc: false
-  })
-  .step({
-    title: 'Step 1',
-    dom: '#step1',
-    showCancelButton: true,
-    cancelButtonText: 'Cancel',
-    preConfirm(popup) {
-      // return false 阻止进入下一步(用于校验)
-      const v = popup.querySelector('input[name="name"]').value.trim();
-      if (!v) return false;
-      return { name: v };
-    }
-  })
-  .step({
-    title: 'Step 2',
-    dom: '#step2',
-    preConfirm(popup) {
-      return { plan: popup.querySelector('select[name="plan"]').value };
-    }
-  })
+Layer.$({ icon: 'info', closeOnEsc: false })
+  .step({ title: 'Step 1', dom: '#step1', showCancelButton: true })
+  .step({ title: 'Step 2', dom: '#step2' })
   .run()
   .then((res) => {
-    if (res.isConfirmed) {
-      // res.value 是每一步 preConfirm 的返回值数组
-      console.log(res.value);
-    }
-  });
-});
-```
-
-更简单的“容器式 steps”写法(适合表单收集):
-
-```js
-$('#step').click(function () {
-  $.layer({
-    step: '#stepContainer',      // 容器
-    stepItem: '.stepItem',       // 可选:没有则默认取容器的一级子元素
-    title: 'Create order',       // 其他 Layer 选项仍可用(作为 base)
-    showCancelButton: true
-  }).then((res) => {
-    if (res.isConfirmed) {
-      // res.data 为统一汇总的表单数据(来自 steps 内所有 input/select/textarea)
-      console.log(res.data);
-    }
+    if (res.isConfirmed) console.log(res.value);
   });
-});
-```
-
-补充说明:
-
-- `stepItem` 不传时,会自动用 `#stepContainer` 的一级子元素作为每一步
-- 单步标题可由每个 step DOM 自身提供:`data-step-title` / `data-layer-title` / `title`
-- `res.data` 会汇总 steps 内所有 `input/select/textarea` 的值(适合一次性提交)
-- Steps 按钮可加图标:`nextIcon`(下一步)/ `prevIcon`(上一步),支持 HTML 字符串或 DOM 节点
-- `confirmButtonColor` / `cancelButtonColor` 已改为 CSS 控制(见 `xjs.css` 里的 `--layer-confirm-*` / `--layer-cancel-*`)
-
-也支持便捷写法:
-
-```js
-Layer.flow([
-  { title: 'Step 1', dom: '#step1' },
-  { title: 'Step 2', dom: '#step2' }
-], { icon: 'info' });
-```
-
-### 11.3 背景动画 / 背景图案(Layer popup 背景)
-
-Layer 现支持为 `layer-popup` 设置整体背景(可与 `icon` 同时使用):
-
-- **Lottie SVG**:`background: 'svg(BallSorting.json)'`
-- **图片**:`background: 'url("/images/xxx.jpg")'`
-- **CSS**:`background: 'css(filter: saturate(1.1); background: linear-gradient(...))'`
-
-说明:
-
-- 背景会铺满弹窗区域(100% 宽高)
-- 不设置 `icon` 时不会显示 icon 行
-- 每个 step 或单独的 layer 都可设置 `background`
-- Steps 模式下:**背景不一致时**随内容左右切换,**背景一致时**仅切换内容
-
-示例(单层):
-
-```js
-$.layer({
-  title: 'Background demo',
-  text: 'Popup background',
-  background: 'svg(BallSorting.json)',
-  icon: 'svg:BallSorting'
-});
-```
-
-示例(Steps):
-
-```js
-$.layer({
-  title: 'Step 1',
-  text: 'Background A',
-  background: 'svg(BallSorting.json)',
-  showCancelButton: true
-})
-.step({
-  title: 'Step 2',
-  text: 'Background B',
-  background: 'url("/images/hero.jpg")'
-})
-.run();
-```
-
-### 11.4 弹窗尺寸(宽/高)
-
-Layer 支持直接指定弹窗宽高:
-
-- `width`:宽度(例如 `320` / `'320px'` / `'24em'`)
-- `height`:高度(例如 `240` / `'240px'` / `'40vh'`)
-
-说明:
-
-- 不设置时遵循 `xjs.css` 的默认自适应(`fit-content` + `min-width`)
-- 设置任一尺寸后,**该尺寸优先**,超出区域会自动出现对应滚动条(可能双向)
- - 传入数字时默认按 `px` 处理;字符串可用任意 CSS 单位(`px`/`em`/`rem`/`vw`/`vh`/`%` 等)
-
-示例:
-
-```js
-$.layer({
-  title: 'Fixed size',
-  text: 'Popup with fixed size',
-  width: 360,
-  height: 220
-});
 ```
 

+ 17 - 4
doc/layer/test_custom_animation.html

@@ -99,6 +99,7 @@
   const iconAnimation = !!document.getElementById('optIcon')?.checked;
   const popupAnimation = !!document.getElementById('optPopup')?.checked;
   const closeOnEsc = !!document.getElementById('optEsc')?.checked;
+  const iconBox = name === 'loadding' ? '11em' : null;
 
   const titleCap = name ? (name[0].toUpperCase() + name.slice(1)) : '';
 
@@ -108,7 +109,9 @@
     icon: 'svg:' + name,
     iconAnimation,
     popupAnimation,
-    closeOnEsc
+    closeOnEsc,
+    iwidth: iconBox,
+    iheight: iconBox
   });
 }</pre>
 
@@ -228,6 +231,7 @@
       const closeOnEsc = !!document.getElementById('optEsc')?.checked;
 
       const titleCap = name ? (name[0].toUpperCase() + name.slice(1)) : '';
+      const iconBox = name === 'loadding' ? '11em' : null;
 
       // Keep the displayed code consistent with what is executed.
       const code = `$.layer({
@@ -236,10 +240,17 @@
   icon: 'svg:${name}',
   iconAnimation: ${iconAnimation},
   popupAnimation: ${popupAnimation},
-  closeOnEsc: ${closeOnEsc}
+  closeOnEsc: ${closeOnEsc},
+  iwidth: ${iconBox ? `'${iconBox}'` : 'null'},
+  iheight: ${iconBox ? `'${iconBox}'` : 'null'}
 });`;
       const jsPre = document.getElementById('js-code');
-      if (jsPre) jsPre.textContent = code;
+      if (jsPre) {
+        jsPre.textContent = code;
+        // Re-run syntax highlight after content change.
+        try { jsPre.dataset.highlighted = '0'; } catch {}
+        try { if (window.__highlightCssViews) window.__highlightCssViews(); } catch {}
+      }
 
       $.layer({
         title: titleCap,
@@ -247,7 +258,9 @@
         icon: 'svg:' + name,
         iconAnimation,
         popupAnimation,
-        closeOnEsc
+        closeOnEsc,
+        iwidth: iconBox,
+        iheight: iconBox
       });
     }
 

+ 6 - 0
doc/layer/test_custom_animation_background.html

@@ -123,6 +123,7 @@ function openSingle(type, sizeKey) {
     title: 'Background: ' + type.toUpperCase(),
     text: 'Layer popup background demo.',
     background: BACKGROUNDS[type],
+    theme: 'light',
     width: size ? size.width : null,
     height: size ? size.height : null,
     icon: showIcon ? 'svg:BallSorting' : null,
@@ -144,6 +145,7 @@ function openSteps(mode) {
     title: 'Step 1',
     text: 'Background should follow when different.',
     background: mode === 'same' ? same : a,
+    theme: 'light',
     icon: showIcon ? 'info' : null,
     popupAnimation,
     closeOnEsc,
@@ -306,6 +308,7 @@ function openSteps(mode) {
   title: 'Background: ${type.toUpperCase()}',
   text: 'Layer popup background demo.',
   background: '${BACKGROUNDS[type]}',
+  theme: 'light',
   width: ${size ? JSON.stringify(size.width) : 'null'},
   height: ${size ? JSON.stringify(size.height) : 'null'},
   icon: ${showIcon ? "'svg:BallSorting'" : 'null'},
@@ -318,6 +321,7 @@ function openSteps(mode) {
         title: 'Background: ' + type.toUpperCase(),
         text: 'Layer popup background demo.',
         background: BACKGROUNDS[type],
+        theme: 'light',
         width: size ? size.width : null,
         height: size ? size.height : null,
         icon: showIcon ? 'svg:BallSorting' : null,
@@ -340,6 +344,7 @@ function openSteps(mode) {
   title: 'Step 1',
   text: 'Background should follow when different.',
   background: ${mode === 'same' ? 'same' : 'a'},
+  theme: 'light',
   icon: ${showIcon ? "'info'" : 'null'},
   popupAnimation: ${popupAnimation},
   closeOnEsc: ${closeOnEsc},
@@ -366,6 +371,7 @@ function openSteps(mode) {
         title: 'Step 1',
         text: 'Background should follow when different.',
         background: mode === 'same' ? same : a,
+        theme: 'light',
         icon: showIcon ? 'info' : null,
         popupAnimation,
         closeOnEsc,

+ 135 - 81
layer.js

@@ -38,9 +38,16 @@
   const PREFIX = 'layer-';
   const RING_TYPES = new Set(['success', 'error', 'warning', 'info', 'question']);
   const RING_BG_PARTS_SELECTOR = `.${PREFIX}success-circular-line-left, .${PREFIX}success-circular-line-right, .${PREFIX}success-fix`;
-  const LOTTIE_PREFIX = 'svg:';
+  const SVGANI_PREFIX = 'svg:';
   const BG_SVG_PREFIX = 'svg(';
   const BG_CSS_PREFIX = 'css(';
+  const THEME_VALUES = new Set(['auto', 'dark', 'light']);
+
+  const normalizeTheme = (value) => {
+    if (typeof value !== 'string') return 'auto';
+    const v = value.trim().toLowerCase();
+    return THEME_VALUES.has(v) ? v : 'auto';
+  };
 
   const normalizeOptions = (options, text, icon) => {
     if (typeof options !== 'string') return options || {};
@@ -116,24 +123,24 @@
     }
   };
 
-  // Lottie (svg.js) lazy loader for "svg:*" icon type
-  let _lottieReady = null;
-  let _lottiePrefetchScheduled = false;
-  const isLottieIcon = (icon) => {
+  // SvgAni (svg.js) lazy loader for "svg:*" icon type
+  let _svgAniReady = null;
+  let _svgAniPrefetchScheduled = false;
+  const isSvgAniIcon = (icon) => {
     if (typeof icon !== 'string') return false;
     const s = icon.trim();
-    return s.startsWith(LOTTIE_PREFIX);
+    return s.startsWith(SVGANI_PREFIX);
   };
-  const getLottieIconName = (icon) => {
-    if (!isLottieIcon(icon)) return '';
-    let name = String(icon || '').trim().slice(LOTTIE_PREFIX.length).trim();
+  const getSvgAniIconName = (icon) => {
+    if (!isSvgAniIcon(icon)) return '';
+    let name = String(icon || '').trim().slice(SVGANI_PREFIX.length).trim();
     if (name === 'loadding') name = 'loading';
     return name;
   };
-  const normalizeLottieName = (raw) => {
+  const normalizeSvgAniName = (raw) => {
     let name = String(raw || '').trim();
     if (!name) return '';
-    if (name.startsWith(LOTTIE_PREFIX)) name = getLottieIconName(name);
+    if (name.startsWith(SVGANI_PREFIX)) name = getSvgAniIconName(name);
     if (name === 'loadding') name = 'loading';
     return name;
   };
@@ -152,11 +159,11 @@
       return '';
     }
   };
-  const resolveLottieScriptUrl = () => {
+  const resolveSvgAniScriptUrl = () => {
     const base = getLayerScriptBase();
     return base ? (base + 'svg/svg.js') : 'svg/svg.js';
   };
-  const resolveLottieJsonUrl = (name) => {
+  const resolveSvgAniJsonUrl = (name) => {
     const raw = String(name || '').trim();
     if (!raw) return '';
     if (/^(https?:)?\/\//.test(raw)) return raw;
@@ -165,15 +172,15 @@
     const file = raw.endsWith('.json') ? raw : (raw + '.json');
     return (base ? base : '') + 'svg/' + file;
   };
-  const ensureLottieLib = () => {
+  const ensureSvgAniLib = () => {
     try {
       if (typeof window === 'undefined') return Promise.resolve(null);
       if (window.lottie) return Promise.resolve(window.lottie);
       if (typeof document === 'undefined' || !document.head) return Promise.resolve(null);
-      if (_lottieReady) return _lottieReady;
+      if (_svgAniReady) return _svgAniReady;
 
-      const existing = document.getElementById('layer-lottie-lib');
-      _lottieReady = new Promise((resolve) => {
+      const existing = document.getElementById('layer-svgani-lib');
+      _svgAniReady = new Promise((resolve) => {
         const finish = () => resolve(window.lottie || null);
         if (existing) {
           try { existing.addEventListener('load', finish, { once: true }); } catch {}
@@ -182,33 +189,50 @@
           return;
         }
         const script = document.createElement('script');
-        script.id = 'layer-lottie-lib';
+        script.id = 'layer-svgani-lib';
         script.async = true;
-        script.src = resolveLottieScriptUrl();
+        script.src = resolveSvgAniScriptUrl();
         try { script.addEventListener('load', finish, { once: true }); } catch {}
         try { script.addEventListener('error', finish, { once: true }); } catch {}
         setTimeout(finish, 1800);
         document.head.appendChild(script);
       });
-      return _lottieReady;
+      return _svgAniReady;
     } catch {
       return Promise.resolve(null);
     }
   };
-  const scheduleLottiePrefetch = () => {
-    if (_lottiePrefetchScheduled) return;
-    _lottiePrefetchScheduled = true;
+  const scheduleSvgAniPrefetch = () => {
+    if (_svgAniPrefetchScheduled) return;
+    _svgAniPrefetchScheduled = true;
     try {
       if (typeof window === 'undefined') return;
       const start = () => {
         setTimeout(() => {
-          try { ensureLottieLib(); } catch {}
+          try { ensureSvgAniLib(); } catch {}
         }, 0);
       };
       if (document.readyState === 'complete') start();
       else window.addEventListener('load', start, { once: true });
     } catch {}
   };
+  const waitForConnected = (el) => new Promise((resolve) => {
+    if (!el) return resolve(false);
+    if (el.isConnected) return resolve(true);
+    let done = false;
+    const finish = () => {
+      if (done) return;
+      done = true;
+      resolve(!!el.isConnected);
+    };
+    try {
+      requestAnimationFrame(() => requestAnimationFrame(finish));
+    } catch {
+      setTimeout(finish, 0);
+      return;
+    }
+    setTimeout(finish, 120);
+  });
 
   class Layer {
     constructor() {
@@ -321,7 +345,7 @@
         text: '',
         icon: null,
         iconSize: null, // e.g. '6em' / '72px'
-        iconLoop: null, // lottie only: true/false/null (auto)
+        iconLoop: null, // svgAni only: true/false/null (auto)
         confirmButtonText: 'OK',
         cancelButtonText: 'Cancel',
         showCancelButton: false,
@@ -329,9 +353,12 @@
         closeOnEsc: true,
         iconAnimation: true,
         popupAnimation: true,
+        theme: 'auto',
         background: null,
         width: null,
         height: null,
+        iwidth: null,
+        iheight: null,
         nextIcon: null, // flow-only: icon for Next button
         prevIcon: null, // flow-only: icon for Back button
         // Content:
@@ -434,7 +461,7 @@
     }
 
     _render(meta = null) {
-      this._destroyLottieIcon();
+      this._destroySvgAniIcon();
       // Remove existing if any (but first, try to restore any mounted DOM from the previous instance)
       const existing = document.querySelector(`.${PREFIX}overlay`);
       const wantReplace = !!(this.params && this.params.replace);
@@ -489,6 +516,7 @@
       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 });
@@ -589,7 +617,7 @@
     }
 
     _renderFlowUI() {
-      this._destroyLottieIcon();
+      this._destroySvgAniIcon();
       // Icon/title/content/actions are stable; steps are pre-mounted and toggled.
       const popup = this.dom.popup;
       if (!popup) return;
@@ -598,6 +626,7 @@
       while (popup.firstChild) popup.removeChild(popup.firstChild);
 
       this._applyPopupSize(this.params);
+      this._applyPopupTheme(this.params);
 
       // Background (full popup)
       this._applyBackgroundForOptions(this.params, { force: true });
@@ -718,6 +747,13 @@
     _createIcon(type) {
       const rawType = String(type || '').trim();
       const icon = el('div', `${PREFIX}icon ${rawType}`);
+      const applyIconBoxSize = () => {
+        if (!this.params) return;
+        const w = this._normalizeSizeValue(this.params.iwidth);
+        const h = this._normalizeSizeValue(this.params.iheight);
+        if (w) icon.style.width = w;
+        if (h) icon.style.height = h;
+      };
       const applyIconSize = (mode) => {
         if (!(this.params && this.params.iconSize)) return;
         try {
@@ -793,16 +829,17 @@
         }
       };
       
-      if (isLottieIcon(rawType)) {
-        // Lottie animation icon: svg:<name>
-        const name = getLottieIconName(rawType);
-        icon.className = `${PREFIX}icon ${PREFIX}icon-lottie`;
-        icon.dataset.lottie = name;
-        const container = el('div', `${PREFIX}lottie`);
+      if (isSvgAniIcon(rawType)) {
+        // SvgAni animation icon: svg:<name>
+        const name = getSvgAniIconName(rawType);
+        icon.className = `${PREFIX}icon ${PREFIX}icon-svgAni`;
+        icon.dataset.svgani = name;
+        const container = el('div', `${PREFIX}svgAni`);
         icon.appendChild(container);
         applyIconSize('box');
-        scheduleLottiePrefetch();
-        this._initLottieIcon(icon, name);
+        applyIconBoxSize();
+        scheduleSvgAniPrefetch();
+        this._initSvgAniIcon(icon, name);
         return icon;
       }
 
@@ -831,6 +868,7 @@
         appendRingParts();
 
         applyIconSize('font');
+        applyIconBoxSize();
 
         // SVG only draws the inner symbol (no SVG ring)
         const svg = createInnerMarkSvg();
@@ -841,6 +879,7 @@
 
       // Default to SVG icons for other/custom types
       applyIconSize('box');
+      applyIconBoxSize();
 
       const svg = createInnerMarkSvg();
 
@@ -887,17 +926,29 @@
       popup.style.overflow = 'auto';
     }
 
+    _applyPopupTheme(options) {
+      const popup = this.dom && this.dom.popup;
+      if (!popup) return;
+      const theme = normalizeTheme(options && options.theme);
+      try {
+        popup.classList.remove(`${PREFIX}theme-auto`, `${PREFIX}theme-light`, `${PREFIX}theme-dark`);
+      } catch {}
+      try {
+        popup.classList.add(`${PREFIX}theme-${theme}`);
+      } catch {}
+    }
+
     _normalizeBackgroundSpec(raw) {
       if (!raw || typeof raw !== 'string') return null;
       const s = raw.trim();
       if (!s) return null;
       if (s.startsWith(BG_SVG_PREFIX) && s.endsWith(')')) {
-        const name = normalizeLottieName(s.slice(BG_SVG_PREFIX.length, -1));
+        const name = normalizeSvgAniName(s.slice(BG_SVG_PREFIX.length, -1));
         if (!name) return null;
         return { type: 'svg', name, key: `svg:${name}` };
       }
-      if (s.startsWith(LOTTIE_PREFIX)) {
-        const name = normalizeLottieName(s);
+      if (s.startsWith(SVGANI_PREFIX)) {
+        const name = normalizeSvgAniName(s);
         if (!name) return null;
         return { type: 'svg', name, key: `svg:${name}` };
       }
@@ -934,9 +985,9 @@
       } else if (spec.type === 'css') {
         try { bg.style.cssText += ';' + spec.css; } catch {}
       } else if (spec.type === 'svg') {
-        const container = el('div', `${PREFIX}lottie`);
+        const container = el('div', `${PREFIX}svgAni`);
         bg.appendChild(container);
-        this._initLottieBackground(bg, spec.name);
+        this._initSvgAniBackground(bg, spec.name);
       }
       return bg;
     }
@@ -947,7 +998,7 @@
       const spec = this._normalizeBackgroundSpec(options && options.background);
       if (!spec) {
         if (this.dom.background) {
-          this._destroyLottieBackground(this.dom.background);
+          this._destroySvgAniBackground(this.dom.background);
           try { this.dom.background.remove(); } catch {}
         }
         this.dom.background = null;
@@ -957,7 +1008,7 @@
       }
       if (!force && this._isSameBackgroundSpec(this._backgroundSpec, spec)) return;
       if (this.dom.background) {
-        this._destroyLottieBackground(this.dom.background);
+        this._destroySvgAniBackground(this.dom.background);
         try { this.dom.background.remove(); } catch {}
       }
       const bgEl = this._createBackgroundEl(spec);
@@ -1029,7 +1080,7 @@
       } catch {}
 
       if (fromEl) {
-        this._destroyLottieBackground(fromEl);
+        this._destroySvgAniBackground(fromEl);
         try { fromEl.remove(); } catch {}
       }
       if (toEl) {
@@ -1040,29 +1091,29 @@
       this._backgroundSpec = nextSpec || null;
     }
 
-    _destroyLottieBackground(bgEl) {
+    _destroySvgAniBackground(bgEl) {
       if (!bgEl) return;
-      const inst = bgEl._lottieInstance;
+      const inst = bgEl._svgAniInstance;
       if (inst && typeof inst.destroy === 'function') {
         try { inst.destroy(); } catch {}
       }
-      try { bgEl._lottieInstance = null; } catch {}
+      try { bgEl._svgAniInstance = null; } catch {}
     }
 
-    _initLottieBackground(bgEl, name) {
+    _initSvgAniBackground(bgEl, name) {
       if (!bgEl) return;
-      const container = bgEl.querySelector(`.${PREFIX}lottie`);
+      const container = bgEl.querySelector(`.${PREFIX}svgAni`);
       if (!container) return;
-      const cleanName = normalizeLottieName(name);
+      const cleanName = normalizeSvgAniName(name);
       if (!cleanName) return;
 
-      const url = resolveLottieJsonUrl(cleanName);
+      const url = resolveSvgAniJsonUrl(cleanName);
       if (!url) return;
 
       const showError = (msg) => {
         try {
           container.textContent = String(msg || '');
-          container.classList.add(`${PREFIX}lottie-error`);
+          container.classList.add(`${PREFIX}svgAni-error`);
         } catch {}
       };
 
@@ -1094,25 +1145,25 @@
       });
 
       const boot = async () => {
-        const lottie = await ensureLottieLib();
-        if (!lottie) {
-          showError('Lottie lib not available');
+        const svgAniLib = await ensureSvgAniLib();
+        if (!svgAniLib) {
+          showError('SvgAni lib not available');
           return;
         }
         try {
           const data = await loadData();
           if (!data) throw new Error('empty json');
           try { container.textContent = ''; } catch {}
-          const anim = lottie.loadAnimation({
+          const anim = svgAniLib.loadAnimation({
             container,
             renderer: 'svg',
             loop: true,
             autoplay: true,
             animationData: data
           });
-          bgEl._lottieInstance = anim;
+          bgEl._svgAniInstance = anim;
         } catch (e) {
-          showError('Lottie load failed');
+          showError('SvgAni load failed');
         }
       };
 
@@ -1239,7 +1290,7 @@
       const icon = this.dom.icon;
       if (!icon) return;
       const type = (this.params && this.params.icon) || '';
-      if (isLottieIcon(type)) return;
+      if (isSvgAniIcon(type)) return;
 
       // Ring animation (same as success) for all built-in icons
       if (RING_TYPES.has(type)) this._adjustRingBackgroundColor();
@@ -1295,8 +1346,8 @@
     }
 
     _forceDestroy(reason = 'replace') {
-      try { this._destroyLottieIcon(); } catch {}
-      try { this._destroyLottieBackground(this.dom && this.dom.background); } catch {}
+      try { this._destroySvgAniIcon(); } catch {}
+      try { this._destroySvgAniBackground(this.dom && this.dom.background); } catch {}
       // Restore mounted DOM (if any) and cleanup listeners; also resolve the promise so it doesn't hang.
       try {
         if (this._flowSteps && this._flowSteps.length) this._unmountFlowMounted();
@@ -1316,8 +1367,8 @@
     _close(isConfirmed, reason) {
       if (this._isClosing) return;
       this._isClosing = true;
-      try { this._destroyLottieIcon(); } catch {}
-      try { this._destroyLottieBackground(this.dom && this.dom.background); } catch {}
+      try { this._destroySvgAniIcon(); } catch {}
+      try { this._destroySvgAniBackground(this.dom && this.dom.background); } catch {}
 
       const shouldDelayUnmount = !!(
         (this._mounted && this._mounted.kind === 'move') ||
@@ -1857,6 +1908,7 @@
       // Apply new options (title/buttons/icon policy) before showing the pane
       this._flowIndex = nextIndex;
       this.params = nextOptions;
+      this._applyPopupTheme(this.params);
 
       // Update icon/title/buttons without recreating DOM
       try {
@@ -1871,14 +1923,14 @@
       try {
         const wantsIcon = !!this.params.icon;
         if (!wantsIcon && this.dom.icon) {
-          this._destroyLottieIcon();
+          this._destroySvgAniIcon();
           this.dom.icon.remove();
           this.dom.icon = null;
         } else if (wantsIcon) {
           const same = this._isSameIconType(this.dom.icon, this.params.icon);
           if (!this.dom.icon || !same) {
             if (this.dom.icon) {
-              this._destroyLottieIcon();
+              this._destroySvgAniIcon();
               this.dom.icon.remove();
             }
             this.dom.icon = this._createIcon(this.params.icon);
@@ -1983,9 +2035,9 @@
     _isSameIconType(iconEl, type) {
       if (!iconEl) return false;
       const raw = String(type || '').trim();
-      if (isLottieIcon(raw)) {
-        const name = getLottieIconName(raw);
-        return iconEl.dataset && iconEl.dataset.lottie === name;
+      if (isSvgAniIcon(raw)) {
+        const name = getSvgAniIconName(raw);
+        return iconEl.dataset && iconEl.dataset.svgani === name;
       }
       try {
         return iconEl.classList && iconEl.classList.contains(raw);
@@ -1994,24 +2046,24 @@
       }
     }
 
-    _destroyLottieIcon() {
+    _destroySvgAniIcon() {
       const icon = this.dom && this.dom.icon;
       if (!icon) return;
-      const inst = icon._lottieInstance;
+      const inst = icon._svgAniInstance;
       if (inst && typeof inst.destroy === 'function') {
         try { inst.destroy(); } catch {}
       }
-      try { icon._lottieInstance = null; } catch {}
+      try { icon._svgAniInstance = null; } catch {}
     }
 
-    _initLottieIcon(iconEl, name) {
+    _initSvgAniIcon(iconEl, name) {
       if (!iconEl) return;
-      const container = iconEl.querySelector(`.${PREFIX}lottie`);
+      const container = iconEl.querySelector(`.${PREFIX}svgAni`);
       if (!container) return;
       const cleanName = String(name || '').trim();
       if (!cleanName) return;
 
-      const url = resolveLottieJsonUrl(cleanName);
+      const url = resolveSvgAniJsonUrl(cleanName);
       if (!url) return;
 
       const wantsLoop = (this.params && typeof this.params.iconLoop === 'boolean')
@@ -2022,7 +2074,7 @@
       const showError = (msg) => {
         try {
           container.textContent = String(msg || '');
-          container.classList.add(`${PREFIX}lottie-error`);
+          container.classList.add(`${PREFIX}svgAni-error`);
         } catch {}
       };
 
@@ -2054,12 +2106,13 @@
       });
 
       (async () => {
-        const lottie = await ensureLottieLib();
-        if (!lottie) {
-          showError('Lottie lib not loaded.');
+        const svgAniLib = await ensureSvgAniLib();
+        if (!svgAniLib) {
+          showError('SvgAni lib not loaded.');
           return;
         }
-        if (!iconEl.isConnected) return;
+        const connected = await waitForConnected(iconEl);
+        if (!connected) return;
         let data = null;
         try { data = await loadData(); } catch {}
         if (!data) {
@@ -2068,14 +2121,14 @@
         }
         if (!iconEl.isConnected) return;
         try {
-          const anim = lottie.loadAnimation({
+          const anim = svgAniLib.loadAnimation({
             container,
             renderer: 'svg',
             loop: wantsLoop,
             autoplay: wantsAutoplay,
             animationData: data
           });
-          iconEl._lottieInstance = anim;
+          iconEl._svgAniInstance = anim;
           if (!wantsAutoplay && anim && typeof anim.goToAndStop === 'function') {
             anim.goToAndStop(0, true);
           }
@@ -2086,13 +2139,14 @@
     }
 
     _rerenderInside() {
-      this._destroyLottieIcon();
+      this._destroySvgAniIcon();
       // Update popup content without recreating overlay (used in flow transitions)
       const popup = this.dom && this.dom.popup;
       if (!popup) return;
 
       // Clear popup (but keep reference)
       while (popup.firstChild) popup.removeChild(popup.firstChild);
+      this._applyPopupTheme(this.params);
 
       // Icon
       this.dom.icon = null;

+ 43 - 11
xjs.css

@@ -45,6 +45,30 @@
   --layer-confirm-hover: #2b77c0;
   --layer-cancel-bg: #444;
   --layer-cancel-hover: #333;
+  --layer-title-color: #333;
+  --layer-content-color: #545454;
+  --layer-button-text-color: #fff;
+  --layer-muted-color: #9a9a9a;
+}
+.layer-popup.layer-theme-light {
+  --layer-title-color: #f3f3f3;
+  --layer-content-color: #cfcfcf;
+  --layer-button-text-color: #f3f3f3;
+  --layer-muted-color: #a7a7a7;
+}
+.layer-popup.layer-theme-dark {
+  --layer-title-color: #333;
+  --layer-content-color: #545454;
+  --layer-button-text-color: #fff;
+  --layer-muted-color: #9a9a9a;
+}
+@media (prefers-color-scheme: dark) {
+  .layer-popup.layer-theme-auto {
+    --layer-title-color: #f3f3f3;
+    --layer-content-color: #cfcfcf;
+    --layer-button-text-color: #f3f3f3;
+    --layer-muted-color: #a7a7a7;
+  }
 }
 .layer-popup > :not(.layer-bg) {
   position: relative;
@@ -59,12 +83,12 @@
   background-size: 100% 100%;
   pointer-events: none;
 }
-.layer-bg .layer-lottie {
+.layer-bg .layer-svgAni {
   width: 100%;
   height: 100%;
   display: block;
 }
-.layer-bg .layer-lottie svg {
+.layer-bg .layer-svgAni svg {
   width: 100%;
   height: 100%;
   display: block;
@@ -75,19 +99,26 @@
 .layer-title {
   font-size: 1.8em;
   font-weight: 600;
-  color: #333;
+  color: var(--layer-title-color);
   margin: 0 0 0.5em;
   text-align: center;
 }
 .layer-content {
   font-size: 1.125em;
-  color: #545454;
+  color: var(--layer-content-color);
   margin-bottom: 1.5em;
   text-align: center;
   line-height: 1.5;
   width: auto;
   max-width: 100%;
 }
+/* Ensure demo page global h2 styles don't override theme */
+.layer-popup .layer-title {
+  color: var(--layer-title-color);
+}
+.layer-popup .layer-content {
+  color: var(--layer-content-color);
+}
 .layer-actions {
   display: flex;
   justify-content: center;
@@ -101,7 +132,7 @@
   font-size: 1em;
   cursor: pointer;
   transition: background-color 0.2s, box-shadow 0.2s;
-  color: #fff;
+  color: var(--layer-button-text-color);
   display: inline-flex;
   align-items: center;
   gap: 0.45em;
@@ -132,7 +163,7 @@
   box-sizing: content-box;
   width: 5em;
   height: 5em;
-  margin: 0.5em auto 1.8em;
+  margin: 0 auto 1em;
   border: 0.25em solid rgba(0, 0, 0, 0);
   border-radius: 50%;
   font-family: inherit;
@@ -144,25 +175,26 @@
   /* End is one full turn from start so it "sweeps then settles" */
   --layer-ring-end-rotate: -405deg;
 }
-.layer-icon.layer-icon-lottie {
+.layer-icon.layer-icon-svgAni {
   border: none;
   padding: 0;
   display: grid;
   place-items: center;
+  margin: -1em auto -1em;
 }
-.layer-icon .layer-lottie {
+.layer-icon .layer-svgAni {
   width: 100%;
   height: 100%;
   display: block;
 }
-.layer-icon .layer-lottie svg {
+.layer-icon .layer-svgAni svg {
   width: 100%;
   height: 100%;
   display: block;
 }
-.layer-lottie-error {
+.layer-svgAni-error {
   font-size: 11px;
-  color: #9a9a9a;
+  color: var(--layer-muted-color);
   text-align: center;
   line-height: 1.2;
 }