robert 1 dzień temu
rodzic
commit
968083d8c7
7 zmienionych plików z 734 dodań i 1018 usunięć
  1. 239 0
      Guide.md
  2. 2 0
      README.md
  3. 35 283
      dist/xjs.js
  4. 1 0
      doc/index.html
  5. 134 456
      layer.js
  6. 286 0
      xjs.css
  7. 37 279
      xjs.js

+ 239 - 0
Guide.md

@@ -0,0 +1,239 @@
+# XJS 项目开发指南(Guide)
+
+本仓库的目标是做一套**可用于生产环境**的 JS + CSS 框架,最终在业务项目中只需要引入:
+
+- `xjs.js`(单文件 JS)
+- `xjs.css`(单文件 CSS)
+
+但在本仓库内,为了保持可维护性,**开发源码仍然分散在多个文件中**(例如 `animal.js`、`layer.js`),再通过构建脚本合并为单文件产物。
+
+> 本文档用于统一后续完善/开发的**目录约定、命名规范、编码习惯、测试方式、发布流程**。请把它当成项目的“团队约定”。
+
+---
+
+## 1. 目录结构与职责边界
+
+当前结构(关键文件):
+
+- **`animal.js`**:动画/选择器核心(Selection / animate / svg / timeline / scroll / inView 等能力)
+- **`layer.js`**:弹出层组件(UI + 交互逻辑)
+- **`xjs.css`**:库运行时需要的所有 UI 样式(例如 Layer 的 `.layer-*`)
+- **`xjs.js`**:
+  - 开发阶段:**加载器**(按顺序加载 `animal.js` + `layer.js`)
+  - 同仓库内还可能包含“合并构建版本”的历史内容;开发时**不要手改合并产物**
+- **`dist/xjs.js`**:生产合并产物(由构建命令生成)
+- **`doc/index.html`**:统一测试/文档入口(支持点击导航直接进入示例页面)
+- **`main.go`**:构建与本地静态服务脚本
+
+### 源码与产物的黄金规则
+
+- **只改源码**:`animal.js` / `layer.js` / `xjs.css` / `doc/**`
+- **不手改产物**:`dist/xjs.js`(以及任何由构建生成的合并文件)
+- **任何对外发布**以 `dist/xjs.js + xjs.css` 为准
+
+---
+
+## 2. 本地开发 / 测试流程(推荐路径)
+
+### 2.1 启动静态服务
+
+仓库自带 Go 静态服务(默认端口 8080):
+
+```bash
+go run main.go serve
+```
+
+然后在浏览器打开:
+
+- `http://localhost:8080/doc/index.html`
+
+> 说明:`doc/index.html` 会作为“测试总控台”,左侧导航/中间列表/右侧 iframe 组合用于快速打开各类测试页面。
+
+### 2.2 构建生产单文件(合并)
+
+将 `animal.js + layer.js` 合并输出到 `dist/xjs.js`:
+
+```bash
+go run main.go build
+```
+
+或指定输出路径:
+
+```bash
+go run main.go build path/to/xjs.js
+```
+
+---
+
+## 3. 对外 API / 全局变量规范
+
+### 3.1 全局导出(必须稳定)
+
+对外只承诺以下全局变量(避免污染):
+
+- **`window.xjs`**:主入口(推荐统一使用)
+- **`window.animal`**:兼容别名(与 `xjs` 等价)
+- **`window.Layer`**:Layer 构造/工厂入口
+
+可选行为:
+
+- **不默认导出 `$`**
+- 只有当业务方在引入前显式设置 `window.XJS_GLOBAL_DOLLAR = true` 时,才会在 `$` 未被占用的前提下导出 `$ = xjs`
+
+### 3.2 API 设计原则(统一风格)
+
+- **单入口**:所有能力尽量从 `xjs(selector)` 返回的 `Selection` 链式对象进入
+- **强约束、少魔法**:避免“隐式全局配置”;如需全局配置,集中在 `xjs.config`(未来可扩展)
+- **兼容性优先级**:能用 WAAPI 就用 WAAPI;不支持时优雅降级(必要时 fallback 到 requestAnimationFrame)
+- **返回值规范**:
+  - 异步行为返回 `Promise`(例如 `animate().then(...)`)
+  - 链式/操作型方法返回 `Selection` 以支持链式调用(例如 `.layer(...).addClass(... )` 未来扩展)
+
+---
+
+## 4. JS 命名规范与编码约定
+
+### 4.1 文件/模块命名
+
+- **文件名**:全小写 + 下划线/无分隔(当前采用 `animal.js`、`layer.js`,新增模块保持一致)
+- **模块边界清晰**:
+  - `animal.js`:核心选择器与动画体系(不直接引入 UI 样式)
+  - `layer.js`:UI 组件(允许依赖 `xjs.css`)
+
+### 4.2 变量/函数命名
+
+- **函数**:`camelCase`
+- **类**:`PascalCase`
+- **常量**:`UPPER_SNAKE_CASE`
+- **私有/内部约定**:以下划线前缀标识(例如 `_fire`、`_onKeydown`)
+
+### 4.3 选择器与 Selection 约定
+
+- `xjs(selector)` 返回 `Selection`(继承 Array 的可链式集合)
+- Selection 实例方法命名尽量用动词:`animate / draw / inViewAnimate / layer / unlayer`
+- 新增能力优先做成 Selection 方法,而不是新增全局函数
+
+### 4.4 参数命名与可扩展性
+
+- **Options 对象**:使用 plain object(JSON 友好)
+- **动画参数**:把“动画属性”和“控制参数(duration/easing/loop 等)”分清
+  - 不确定的配置键,不要让它误入“可动画属性”的判定
+  - 建议将“非动画属性”的扩展项放进 `params.options`(现有实现已倾向此策略)
+
+### 4.5 兼容性/健壮性
+
+- 代码应能在“没有 bundler”的脚本环境运行
+- 避免依赖现代语法到不可控程度(如确需使用,确保目标环境可用)
+- 对 DOM/Window 等对象引用前做存在性判断(SSR/非浏览器环境不会直接崩溃)
+
+---
+
+## 5. CSS 规范(`xjs.css`)
+
+### 5.1 单文件策略
+
+- 所有库运行时 UI 样式集中在 `xjs.css`
+- JS 侧**不要**注入大量 `<style>`(可接受“确保已引入 CSS”的兜底逻辑,但不把完整样式写入 JS)
+
+### 5.2 命名空间与前缀(避免污染业务)
+
+目前 Layer 使用 `.layer-*`,后续新增组件建议遵循:
+
+- **组件前缀**:`x-<component>-*` 或沿用当前 `layer-*`(但需保持一致)
+- **推荐做法**:统一前缀策略(后续若决定全库统一 `xjs-`,则逐步迁移)
+
+> 目标是:业务 CSS 不会因为类名冲突而被库“意外影响”,也不会轻易覆盖库样式。
+
+### 5.3 主题化(建议方向)
+
+新增 UI 组件时,尽量通过 CSS 变量暴露可定制项:
+
+- `--xjs-color-primary`
+- `--xjs-radius-md`
+- `--xjs-shadow-lg`
+- `--layer-...`(若继续沿用 layer 命名空间)
+
+并保证:
+
+- **默认值可用**
+- **业务覆盖变量即可换肤**(不需要改库内部选择器)
+
+---
+
+## 6. 文档与测试(`doc/`)
+
+### 6.1 入口约定
+
+- 所有测试页面统一从 `doc/index.html` 进入
+- 新增测试页面时:
+  - 放到合适的子目录(例如 `doc/layer/`、`doc/svg/`、`doc/tween_*`)
+  - 在 `doc/index.html` 的导航树中挂入口,保证“可点击直达”
+
+### 6.2 测试页面规范(建议)
+
+- 每个测试页只验证一类能力(避免“一页测所有导致回归困难”)
+- 页面顶部写清楚:
+  - 测试目标
+  - 预期现象
+  - 参数组合/边界条件
+
+---
+
+## 7. 扩展开发指南(新增能力的推荐落点)
+
+### 7.1 新增动画能力(优先落在 `animal.js`)
+
+典型落点:
+
+- 新增 Selection 方法:`Selection.prototype.xxx = function (...) { ... }`
+- 或在 `Selection` 类体内增加方法(更推荐,保持结构统一)
+- 如需新增静态能力:挂在 `xjs.xxx = ...`(需要同时考虑 `animal` 别名)
+
+### 7.2 新增 UI 组件(独立文件 + CSS )
+
+推荐方式:
+
+- 新增 `component_xxx.js`(或未来统一 `components/xxx.js`)
+- 在 `xjs.css` 增加 `xxx-*`(或 `xjs-xxx-*`)样式
+- 在构建阶段把该模块纳入单文件合并(更新 `main.go build()` 的拼接顺序)
+
+> 当前 `main.go` 固定合并 `animal.js + layer.js`,当模块增长时建议升级为“配置式列表”。
+
+---
+
+## 8. 发布与回归清单(面向生产)
+
+每次准备对外发布(或业务项目更新版本)前建议走一遍:
+
+- **功能回归**
+  - 打开 `doc/index.html`,对本次变更相关页面逐个验证
+- **构建产物**
+  - 执行 `go run main.go build`
+  - 确认 `dist/xjs.js` 更新(不要手改)
+- **对外 API 稳定性**
+  - `xjs/animal/Layer` 是否都存在
+  - `$` 是否仍然是“显式开关才导出”
+- **样式影响面**
+  - `xjs.css` 的类名是否避免与业务冲突
+  - 是否通过 CSS 变量提供可配置项(可选但推荐)
+
+---
+
+## 9. 建议的后续演进(非强制,但推荐)
+
+当模块越来越多时,建议逐步引入以下工程化能力(仍保持“最终只发两个文件”的目标):
+
+- **源码目录化**:例如 `src/animal/`、`src/layer/`、`src/core/`
+- **构建脚本升级**:`main.go` 从“硬编码拼接两个文件”升级为“按清单拼接多个文件”
+- **产物头信息**:在 `dist/xjs.js` 注入版本号、构建时间、变更摘要(便于定位线上问题)
+- **最小化/压缩(可选)**:如需压缩,保持可回溯到未压缩构建(source map 视需求)
+
+---
+
+## 10. 约定优先级
+
+如果本文档与源码现状出现不一致:
+
+1. **以本文档为准**(作为未来统一标准)
+2. 在不破坏对外 API 的前提下,逐步把历史实现迁移到本文档规范
+

+ 2 - 0
README.md

@@ -4,6 +4,8 @@
 
 核心设计理念是 **"All in One Selector"** —— 一个全局函数 `animal` (可别名为 `$`) 既是选择器,也是功能入口。
 
+> 项目贡献/扩展开发的规范与流程请看:`Guide.md`
+
 ## ✨ 核心特性
 
 1.  **极简集成**: 零构建工具依赖,直接引入即可使用。

+ 35 - 283
dist/xjs.js

@@ -43,295 +43,47 @@
 
   const PREFIX = 'layer-';
   
-  // Theme & Styles
-  const css = `
-    .${PREFIX}overlay {
-      position: fixed;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-      background: rgba(0, 0, 0, 0.4);
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      z-index: 1000;
-      opacity: 0;
-      transition: opacity 0.3s;
-    }
-    .${PREFIX}overlay.show {
-      opacity: 1;
-    }
-    .${PREFIX}popup {
-      background: #fff;
-      border-radius: 8px;
-      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
-      width: 32em;
-      max-width: 90%;
-      padding: 1.5em;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      position: relative;
-      z-index: 1;
-      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 {
-      transform: scale(1);
-    }
-    .${PREFIX}title {
-      font-size: 1.8em;
-      font-weight: 600;
-      color: #333;
-      margin: 0 0 0.5em;
-      text-align: center;
-    }
-    .${PREFIX}content {
-      font-size: 1.125em;
-      color: #545454;
-      margin-bottom: 1.5em;
-      text-align: center;
-      line-height: 1.5;
-    }
-    .${PREFIX}actions {
-      display: flex;
-      justify-content: center;
-      gap: 1em;
-      width: 100%;
-    }
-    .${PREFIX}button {
-      border: none;
-      border-radius: 4px;
-      padding: 0.6em 1.2em;
-      font-size: 1em;
-      cursor: pointer;
-      transition: background-color 0.2s, box-shadow 0.2s;
-      color: #fff;
-    }
-    .${PREFIX}confirm {
-      background-color: #3085d6;
-    }
-    .${PREFIX}confirm:hover {
-      background-color: #2b77c0;
-    }
-    .${PREFIX}cancel {
-      background-color: #aaa;
-    }
-    .${PREFIX}cancel:hover {
-      background-color: #999;
-    }
-    .${PREFIX}icon {
-      position: relative;
-      box-sizing: content-box;
-      width: 5em;
-      height: 5em;
-      margin: 2.5em auto 0.6em;
-      border: 0.25em solid rgba(0, 0, 0, 0);
-      border-radius: 50%;
-      font-family: inherit;
-      line-height: 5em;
-      cursor: default;
-      user-select: none;
-    }
-
-    /* SVG mark content (kept), sits above the ring */
-    .${PREFIX}icon svg {
-      width: 100%;
-      height: 100%;
-      display: block;
-      overflow: visible;
-      position: relative;
-      z-index: 3;
-    }
-    .${PREFIX}svg-ring,
-    .${PREFIX}svg-mark {
-      fill: none;
-      stroke-linecap: round;
-      stroke-linejoin: round;
-    }
-    .${PREFIX}svg-ring {
-      stroke-width: 4.5;
-      opacity: 0.95;
-    }
-    .${PREFIX}svg-mark {
-      stroke-width: 6;
-    }
-    .${PREFIX}svg-dot {
-      transform-box: fill-box;
-      transform-origin: center;
-    }
-
-    /* =========================================
-       "success-like" ring (shared by all icons)
-       - we reuse the success ring pieces/rotation for every icon type
-       - color is driven by currentColor
-       ========================================= */
-    .${PREFIX}icon.success { border-color: #a5dc86; color: #a5dc86; }
-    .${PREFIX}icon.error { border-color: #f27474; color: #f27474; }
-    .${PREFIX}icon.warning { border-color: #f8bb86; color: #f8bb86; }
-    .${PREFIX}icon.info { border-color: #3fc3ee; color: #3fc3ee; }
-    .${PREFIX}icon.question { border-color: #b18cff; color: #b18cff; }
-
-    .${PREFIX}icon .${PREFIX}success-ring {
-      position: absolute;
-      z-index: 2;
-      top: -0.25em;
-      left: -0.25em;
-      box-sizing: content-box;
-      width: 100%;
-      height: 100%;
-      border: 0.25em solid currentColor;
-      opacity: 0.3;
-      border-radius: 50%;
-    }
-    .${PREFIX}icon .${PREFIX}success-fix {
-      position: absolute;
-      z-index: 1;
-      top: 0.5em;
-      left: 1.625em;
-      width: 0.4375em;
-      height: 5.625em;
-      transform: rotate(-45deg);
-      background-color: #fff; /* adjusted at runtime to popup bg */
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-left,
-    .${PREFIX}icon .${PREFIX}success-circular-line-right {
-      position: absolute;
-      width: 3.75em;
-      height: 7.5em;
-      border-radius: 50%;
-      background-color: #fff; /* adjusted at runtime to popup bg */
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-left {
-      top: -0.4375em;
-      left: -2.0635em;
-      transform: rotate(-45deg);
-      transform-origin: 3.75em 3.75em;
-      border-radius: 7.5em 0 0 7.5em;
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-right {
-      top: -0.6875em;
-      left: 1.875em;
-      transform: rotate(-45deg);
-      transform-origin: 0 3.75em;
-      border-radius: 0 7.5em 7.5em 0;
-    }
-    .${PREFIX}icon .${PREFIX}success-line-tip,
-    .${PREFIX}icon .${PREFIX}success-line-long {
-      display: block;
-      position: absolute;
-      z-index: 2;
-      height: 0.3125em;
-      border-radius: 0.125em;
-      background-color: currentColor;
-    }
-    .${PREFIX}icon .${PREFIX}success-line-tip {
-      top: 2.875em;
-      left: 0.8125em;
-      width: 1.5625em;
-      transform: rotate(45deg);
-    }
-    .${PREFIX}icon .${PREFIX}success-line-long {
-      top: 2.375em;
-      right: 0.5em;
-      width: 2.9375em;
-      transform: rotate(-45deg);
-    }
-
-    /* Triggered when icon has .${PREFIX}icon-show */
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-tip {
-      animation: ${PREFIX}animate-success-line-tip 0.75s;
-    }
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-long {
-      animation: ${PREFIX}animate-success-line-long 0.75s;
-    }
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
-      animation: ${PREFIX}rotate-success-circular-line 4.25s ease-in;
-    }
-    /* Warning needs a shorter arc (45deg) */
-    .${PREFIX}icon.warning.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
-      animation: ${PREFIX}rotate-warning-circular-line 4.25s ease-in;
-    }
-
-    @keyframes ${PREFIX}animate-success-line-tip {
-      0% {
-        top: 1.1875em;
-        left: 0.0625em;
-        width: 0;
-      }
-      54% {
-        top: 1.0625em;
-        left: 0.125em;
-        width: 0;
-      }
-      70% {
-        top: 2.1875em;
-        left: -0.375em;
-        width: 3.125em;
-      }
-      84% {
-        top: 3em;
-        left: 1.3125em;
-        width: 1.0625em;
-      }
-      100% {
-        top: 2.8125em;
-        left: 0.8125em;
-        width: 1.5625em;
-      }
-    }
-    @keyframes ${PREFIX}animate-success-line-long {
-      0% {
-        top: 3.375em;
-        right: 2.875em;
-        width: 0;
-      }
-      65% {
-        top: 3.375em;
-        right: 2.875em;
-        width: 0;
-      }
-      84% {
-        top: 2.1875em;
-        right: 0;
-        width: 3.4375em;
-      }
-      100% {
-        top: 2.375em;
-        right: 0.5em;
-        width: 2.9375em;
+  // Ensure library CSS is loaded (xjs.css)
+  // - CSS is centralized in xjs.css (no runtime <style> injection).
+  // - We still auto-load it for convenience/compat, since consumers may forget the <link>.
+  const ensureXjsCss = () => {
+    try {
+      if (typeof document === 'undefined') return;
+      if (!document.head) return;
+
+      const existingHref = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
+        .map((l) => (l.getAttribute('href') || '').trim())
+        .find((h) => /(^|\/)xjs\.css(\?|#|$)/.test(h));
+      if (existingHref) return;
+
+      const id = 'xjs-css';
+      if (document.getElementById(id)) return;
+
+      const scripts = Array.from(document.getElementsByTagName('script'));
+      const scriptSrc = scripts
+        .map((s) => s && s.src)
+        .find((src) => /(^|\/)(xjs|layer)\.js(\?|#|$)/.test(String(src || '')));
+
+      let href = 'xjs.css';
+      if (scriptSrc) {
+        href = String(scriptSrc)
+          .replace(/(^|\/)xjs\.js(\?|#|$)/, '$1xjs.css$2')
+          .replace(/(^|\/)layer\.js(\?|#|$)/, '$1xjs.css$2');
       }
-    }
-    @keyframes ${PREFIX}rotate-success-circular-line {
-      0% { transform: rotate(-45deg); }
-      5% { transform: rotate(-45deg); }
-      12% { transform: rotate(-405deg); }
-      100% { transform: rotate(-405deg); }
-    }
-    @keyframes ${PREFIX}rotate-warning-circular-line {
-      0% { transform: rotate(-45deg); }
-      5% { transform: rotate(-45deg); }
-      12% { transform: rotate(-180deg); }
-      100% { transform: rotate(-180deg); } /* match 12% so it "sweeps then stops" like success */
-    }
 
-    /* (Other icon shapes stay SVG-driven; only the ring animation is unified above) */
-  `;
-
-  // Inject Styles
-  const injectStyles = () => {
-    if (document.getElementById(`${PREFIX}styles`)) return;
-    const styleSheet = document.createElement('style');
-    styleSheet.id = `${PREFIX}styles`;
-    styleSheet.textContent = css;
-    document.head.appendChild(styleSheet);
+      const link = document.createElement('link');
+      link.id = id;
+      link.rel = 'stylesheet';
+      link.href = href;
+      document.head.appendChild(link);
+    } catch {
+      // ignore
+    }
   };
 
   class Layer {
     constructor() {
-      injectStyles();
+      ensureXjsCss();
       this.params = {};
       this.dom = {};
       this.promise = null;

+ 1 - 0
doc/index.html

@@ -4,6 +4,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Animal.js Documentation</title>
+    <link rel="stylesheet" href="../xjs.css">
     <style>
         :root {
             --sidebar-bg: #111;

+ 134 - 456
layer.js

@@ -36,302 +36,67 @@
   'use strict';
 
   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 normalizeOptions = (options, text, icon) => {
+    if (typeof options !== 'string') return options || {};
+    const o = { title: options };
+    if (text) o.text = text;
+    if (icon) o.icon = icon;
+    return o;
+  };
   
-  // Theme & Styles
-  const css = `
-    .${PREFIX}overlay {
-      position: fixed;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-      background: rgba(0, 0, 0, 0.4);
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      z-index: 1000;
-      opacity: 0;
-      transition: opacity 0.3s;
-    }
-    .${PREFIX}overlay.show {
-      opacity: 1;
-    }
-    .${PREFIX}popup {
-      background: #fff;
-      border-radius: 8px;
-      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
-      width: 32em;
-      max-width: 90%;
-      padding: 1.5em;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      position: relative;
-      z-index: 1;
-      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 {
-      transform: scale(1);
-    }
-    .${PREFIX}title {
-      font-size: 1.8em;
-      font-weight: 600;
-      color: #333;
-      margin: 0 0 0.5em;
-      text-align: center;
-    }
-    .${PREFIX}content {
-      font-size: 1.125em;
-      color: #545454;
-      margin-bottom: 1.5em;
-      text-align: center;
-      line-height: 1.5;
-    }
-    .${PREFIX}actions {
-      display: flex;
-      justify-content: center;
-      gap: 1em;
-      width: 100%;
-    }
-    .${PREFIX}button {
-      border: none;
-      border-radius: 4px;
-      padding: 0.6em 1.2em;
-      font-size: 1em;
-      cursor: pointer;
-      transition: background-color 0.2s, box-shadow 0.2s;
-      color: #fff;
-    }
-    .${PREFIX}confirm {
-      background-color: #3085d6;
-    }
-    .${PREFIX}confirm:hover {
-      background-color: #2b77c0;
-    }
-    .${PREFIX}cancel {
-      background-color: #aaa;
-    }
-    .${PREFIX}cancel:hover {
-      background-color: #999;
-    }
-    .${PREFIX}icon {
-      position: relative;
-      box-sizing: content-box;
-      width: 5em;
-      height: 5em;
-      margin: 0.5em auto 1.8em;
-      border: 0.25em solid rgba(0, 0, 0, 0);
-      border-radius: 50%;
-      font-family: inherit;
-      line-height: 5em;
-      cursor: default;
-      user-select: none;
-      /* Ring sweep angles (match SweetAlert2 success feel) */
-      --${PREFIX}ring-start-rotate: -45deg;
-      /* End is one full turn from start so it "sweeps then settles" */
-      --${PREFIX}ring-end-rotate: -405deg;
-    }
-
-    /* SVG mark content (kept), sits above the ring */
-    .${PREFIX}icon svg {
-      width: 100%;
-      height: 100%;
-      display: block;
-      overflow: visible;
-      position: relative;
-      z-index: 3;
-    }
-    .${PREFIX}svg-ring,
-    .${PREFIX}svg-mark {
-      fill: none;
-      stroke-linecap: round;
-      stroke-linejoin: round;
-    }
-    .${PREFIX}svg-ring {
-      stroke-width: 4.5;
-      opacity: 0.95;
-    }
-    .${PREFIX}svg-mark {
-      stroke-width: 6;
-    }
-    .${PREFIX}svg-dot {
-      transform-box: fill-box;
-      transform-origin: center;
-    }
-
-    /* =========================================
-       "success-like" ring (shared by all icons)
-       - we reuse the success ring pieces/rotation for every icon type
-       - color is driven by currentColor
-       ========================================= */
-    .${PREFIX}icon.success { border-color: #a5dc86; color: #a5dc86; }
-    .${PREFIX}icon.error { border-color: #f27474; color: #f27474; }
-    .${PREFIX}icon.warning { border-color: #f8bb86; color: #f8bb86; }
-    .${PREFIX}icon.info { border-color: #3fc3ee; color: #3fc3ee; }
-    .${PREFIX}icon.question { border-color: #b18cff; color: #b18cff; }
-
-    /* Keep ring sweep logic identical across built-in icons (match success). */
-
-    .${PREFIX}icon .${PREFIX}success-ring {
-      position: absolute;
-      z-index: 2;
-      top: -0.25em;
-      left: -0.25em;
-      box-sizing: content-box;
-      width: 100%;
-      height: 100%;
-      border: 0.25em solid currentColor;
-      opacity: 0.3;
-      border-radius: 50%;
-    }
-    .${PREFIX}icon .${PREFIX}success-fix {
-      position: absolute;
-      z-index: 1;
-      top: 0.5em;
-      left: 1.625em;
-      width: 0.4375em;
-      height: 5.625em;
-      transform: rotate(-45deg);
-      background-color: #fff; /* adjusted at runtime to popup bg */
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-left,
-    .${PREFIX}icon .${PREFIX}success-circular-line-right {
-      position: absolute;
-      width: 3.75em;
-      height: 7.5em;
-      border-radius: 50%;
-      background-color: #fff; /* adjusted at runtime to popup bg */
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-left {
-      top: -0.4375em;
-      left: -2.0635em;
-      transform: rotate(-45deg);
-      transform-origin: 3.75em 3.75em;
-      border-radius: 7.5em 0 0 7.5em;
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-right {
-      top: -0.6875em;
-      left: 1.875em;
-      transform: rotate(-45deg);
-      transform-origin: 0 3.75em;
-      border-radius: 0 7.5em 7.5em 0;
-    }
-    /* Clip ONLY the success tick (do not clip the ring sweep masks) */
-    .${PREFIX}icon .${PREFIX}success-mark {
-      position: absolute;
-      inset: 0;
-      border-radius: 50%;
-      overflow: hidden;
-      z-index: 3;
-      pointer-events: none;
-    }
-
-    .${PREFIX}icon .${PREFIX}success-line-tip,
-    .${PREFIX}icon .${PREFIX}success-line-long {
-      display: block;
-      position: absolute;
-      z-index: 1; /* inside success-mark; keep above its clipping bg */
-      height: 0.3125em;
-      border-radius: 0.125em;
-      background-color: currentColor;
-    }
-    .${PREFIX}icon .${PREFIX}success-line-tip {
-      top: 2.875em;
-      left: 0.8125em;
-      width: 1.5625em;
-      transform: rotate(45deg);
-    }
-    .${PREFIX}icon .${PREFIX}success-line-long {
-      top: 2.375em;
-      right: 0.5em;
-      width: 2.9375em;
-      transform: rotate(-45deg);
-    }
-
-    /* Triggered when icon has .${PREFIX}icon-show */
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-tip {
-      animation: ${PREFIX}animate-success-line-tip 0.75s;
-    }
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-long {
-      animation: ${PREFIX}animate-success-line-long 0.75s;
-    }
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
-      animation: ${PREFIX}rotate-icon-circular-line 4.25s ease-in;
-    }
-
-    @keyframes ${PREFIX}animate-success-line-tip {
-      0% {
-        top: 1.1875em;
-        left: 0.0625em;
-        width: 0;
-      }
-      54% {
-        top: 1.0625em;
-        left: 0.125em;
-        width: 0;
-      }
-      70% {
-        top: 2.1875em;
-        left: -0.375em;
-        width: 3.125em;
-      }
-      84% {
-        top: 3em;
-        left: 1.3125em;
-        width: 1.0625em;
-      }
-      100% {
-        top: 2.8125em;
-        left: 0.8125em;
-        width: 1.5625em;
-      }
-    }
-    @keyframes ${PREFIX}animate-success-line-long {
-      0% {
-        top: 3.375em;
-        right: 2.875em;
-        width: 0;
-      }
-      65% {
-        top: 3.375em;
-        right: 2.875em;
-        width: 0;
-      }
-      84% {
-        top: 2.1875em;
-        right: 0;
-        width: 3.4375em;
-      }
-      100% {
-        top: 2.375em;
-        right: 0.5em;
-        width: 2.9375em;
+  const el = (tag, className, text) => {
+    const node = document.createElement(tag);
+    if (className) node.className = className;
+    if (text !== undefined) node.textContent = text;
+    return node;
+  };
+  
+  const svgEl = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag);
+  
+  // Ensure library CSS is loaded (xjs.css)
+  // - CSS is centralized in xjs.css (no runtime <style> injection).
+  // - We still auto-load it for convenience/compat, since consumers may forget the <link>.
+  const ensureXjsCss = () => {
+    try {
+      if (typeof document === 'undefined') return;
+      if (!document.head) return;
+
+      const existingHref = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
+        .map((l) => (l.getAttribute('href') || '').trim())
+        .find((h) => /(^|\/)xjs\.css(\?|#|$)/.test(h));
+      if (existingHref) return;
+
+      const id = 'xjs-css';
+      if (document.getElementById(id)) return;
+
+      const scripts = Array.from(document.getElementsByTagName('script'));
+      const scriptSrc = scripts
+        .map((s) => s && s.src)
+        .find((src) => /(^|\/)(xjs|layer)\.js(\?|#|$)/.test(String(src || '')));
+
+      let href = 'xjs.css';
+      if (scriptSrc) {
+        href = String(scriptSrc)
+          .replace(/(^|\/)xjs\.js(\?|#|$)/, '$1xjs.css$2')
+          .replace(/(^|\/)layer\.js(\?|#|$)/, '$1xjs.css$2');
       }
-    }
-    @keyframes ${PREFIX}rotate-icon-circular-line {
-      0% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
-      5% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
-      12% { transform: rotate(var(--${PREFIX}ring-end-rotate)); }
-      100% { transform: rotate(var(--${PREFIX}ring-end-rotate)); } /* match 12% so it "sweeps then stops" */
-    }
 
-    /* (Other icon shapes stay SVG-driven; only the ring animation is unified above) */
-  `;
-
-  // Inject Styles
-  const injectStyles = () => {
-    if (document.getElementById(`${PREFIX}styles`)) return;
-    const styleSheet = document.createElement('style');
-    styleSheet.id = `${PREFIX}styles`;
-    styleSheet.textContent = css;
-    document.head.appendChild(styleSheet);
+      const link = document.createElement('link');
+      link.id = id;
+      link.rel = 'stylesheet';
+      link.href = href;
+      document.head.appendChild(link);
+    } catch {
+      // ignore
+    }
   };
 
   class Layer {
     constructor() {
-      injectStyles();
+      ensureXjsCss();
       this.params = {};
       this.dom = {};
       this.promise = null;
@@ -362,11 +127,7 @@
     // Chainable config helper (does not render until `.fire()` is called)
     config(options = {}) {
       // Support the same shorthand as Layer.fire(title, text, icon)
-      if (typeof options === 'string') {
-        options = { title: options };
-        if (arguments[1]) options.text = arguments[1];
-        if (arguments[2]) options.icon = arguments[2];
-      }
+      options = normalizeOptions(options, arguments[1], arguments[2]);
       this.params = { ...(this.params || {}), ...options };
       return this;
     }
@@ -378,11 +139,7 @@
     }
 
     _fire(options = {}) {
-      if (typeof options === 'string') {
-        options = { title: options };
-        if (arguments[1]) options.text = arguments[1];
-        if (arguments[2]) options.icon = arguments[2];
-      }
+      options = normalizeOptions(options, arguments[1], arguments[2]);
 
       this.params = {
         title: '',
@@ -424,12 +181,10 @@
       if (existing) existing.remove();
 
       // Create Overlay
-      this.dom.overlay = document.createElement('div');
-      this.dom.overlay.className = `${PREFIX}overlay`;
+      this.dom.overlay = el('div', `${PREFIX}overlay`);
 
       // Create Popup
-      this.dom.popup = document.createElement('div');
-      this.dom.popup.className = `${PREFIX}popup`;
+      this.dom.popup = el('div', `${PREFIX}popup`);
       this.dom.overlay.appendChild(this.dom.popup);
 
       // Icon
@@ -440,16 +195,13 @@
 
       // Title
       if (this.params.title) {
-        this.dom.title = document.createElement('h2');
-        this.dom.title.className = `${PREFIX}title`;
-        this.dom.title.textContent = this.params.title;
+        this.dom.title = el('h2', `${PREFIX}title`, this.params.title);
         this.dom.popup.appendChild(this.dom.title);
       }
 
       // Content (Text / HTML / Element)
       if (this.params.text || this.params.html || this.params.content) {
-        this.dom.content = document.createElement('div');
-        this.dom.content.className = `${PREFIX}content`;
+        this.dom.content = el('div', `${PREFIX}content`);
         
         if (this.params.content) {
            // DOM Element or Selector
@@ -466,23 +218,18 @@
       }
 
       // Actions
-      this.dom.actions = document.createElement('div');
-      this.dom.actions.className = `${PREFIX}actions`;
+      this.dom.actions = el('div', `${PREFIX}actions`);
       
       // Cancel Button
       if (this.params.showCancelButton) {
-        this.dom.cancelBtn = document.createElement('button');
-        this.dom.cancelBtn.className = `${PREFIX}button ${PREFIX}cancel`;
-        this.dom.cancelBtn.textContent = this.params.cancelButtonText;
+        this.dom.cancelBtn = el('button', `${PREFIX}button ${PREFIX}cancel`, this.params.cancelButtonText);
         this.dom.cancelBtn.style.backgroundColor = this.params.cancelButtonColor;
         this.dom.cancelBtn.onclick = () => this._close(false);
         this.dom.actions.appendChild(this.dom.cancelBtn);
       }
 
       // Confirm Button
-      this.dom.confirmBtn = document.createElement('button');
-      this.dom.confirmBtn.className = `${PREFIX}button ${PREFIX}confirm`;
-      this.dom.confirmBtn.textContent = this.params.confirmButtonText;
+      this.dom.confirmBtn = el('button', `${PREFIX}button ${PREFIX}confirm`, this.params.confirmButtonText);
       this.dom.confirmBtn.style.backgroundColor = this.params.confirmButtonColor;
       this.dom.confirmBtn.onclick = () => this._close(true);
       this.dom.actions.appendChild(this.dom.confirmBtn);
@@ -508,17 +255,13 @@
     }
 
     _createIcon(type) {
-      const icon = document.createElement('div');
-      icon.className = `${PREFIX}icon ${type}`;
-
+      const icon = el('div', `${PREFIX}icon ${type}`);
       const applyIconSize = (mode) => {
         if (!(this.params && this.params.iconSize)) return;
         try {
-          const raw = this.params.iconSize;
-          const s = String(raw).trim();
+          const s = String(this.params.iconSize).trim();
           if (!s) return;
-          // For SweetAlert2-style success icon, scale via font-size so all `em`-based
-          // parts remain proportional.
+          // For SweetAlert2-style success icon, scale via font-size so all `em`-based parts remain proportional.
           if (mode === 'font') {
             const m = s.match(/^(-?\d*\.?\d+)\s*(px|em|rem)$/i);
             if (m) {
@@ -536,7 +279,58 @@
         } catch {}
       };
       
-      const svgNs = 'http://www.w3.org/2000/svg';
+      const appendRingParts = () => {
+        // Use the same "success-like" ring parts for every built-in icon
+        icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
+        icon.appendChild(el('div', `${PREFIX}success-ring`));
+        icon.appendChild(el('div', `${PREFIX}success-fix`));
+        icon.appendChild(el('div', `${PREFIX}success-circular-line-right`));
+      };
+      
+      const createInnerMarkSvg = () => {
+        const svg = svgEl('svg');
+        svg.setAttribute('viewBox', '0 0 80 80');
+        svg.setAttribute('aria-hidden', 'true');
+        svg.setAttribute('focusable', 'false');
+        return svg;
+      };
+      
+      const addMarkPath = (svg, d, extraClass) => {
+        const p = svgEl('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;
+      };
+      
+      const addDot = (svg, cx, cy) => {
+        const dot = svgEl('circle');
+        dot.setAttribute('class', `${PREFIX}svg-dot`);
+        dot.setAttribute('cx', String(cx));
+        dot.setAttribute('cy', String(cy));
+        dot.setAttribute('r', '3.2');
+        dot.setAttribute('fill', 'currentColor');
+        svg.appendChild(dot);
+        return dot;
+      };
+      
+      const appendBuiltInInnerMark = (svg) => {
+        if (type === 'error') {
+          addMarkPath(svg, 'M28 28 L52 52', `${PREFIX}svg-error-left`);
+          addMarkPath(svg, 'M52 28 L28 52', `${PREFIX}svg-error-right`);
+        } else if (type === 'warning') {
+          addMarkPath(svg, 'M40 20 L40 46', `${PREFIX}svg-warning-line`);
+          addDot(svg, 40, 58);
+        } else if (type === 'info') {
+          addMarkPath(svg, 'M40 34 L40 56', `${PREFIX}svg-info-line`);
+          addDot(svg, 40, 25);
+        } else if (type === 'question') {
+          addMarkPath(svg, 'M30 30 C30 23 35 19 42 19 C49 19 54 23 54 30 C54 36 50 39 46 41 C43 42 42 44 42 48 L42 52', `${PREFIX}svg-question`);
+          addDot(svg, 42, 61);
+        }
+      };
+      
       if (type === 'success') {
         // SweetAlert2-compatible DOM structure (circle + tick) for exact style parity
         //   <div class="...success-circular-line-left"></div>
@@ -544,27 +338,14 @@
         //   <div class="...success-ring"></div>
         //   <div class="...success-fix"></div>
         //   <div class="...success-circular-line-right"></div>
-        const left = document.createElement('div');
-        left.className = `${PREFIX}success-circular-line-left`;
-        const mark = document.createElement('div');
-        mark.className = `${PREFIX}success-mark`;
-        const tip = document.createElement('span');
-        tip.className = `${PREFIX}success-line-tip`;
-        const long = document.createElement('span');
-        long.className = `${PREFIX}success-line-long`;
-        const ring = document.createElement('div');
-        ring.className = `${PREFIX}success-ring`;
-        const fix = document.createElement('div');
-        fix.className = `${PREFIX}success-fix`;
-        const right = document.createElement('div');
-        right.className = `${PREFIX}success-circular-line-right`;
-        icon.appendChild(left);
-        mark.appendChild(tip);
-        mark.appendChild(long);
+        icon.appendChild(el('div', `${PREFIX}success-circular-line-left`));
+        const mark = el('div', `${PREFIX}success-mark`);
+        mark.appendChild(el('span', `${PREFIX}success-line-tip`));
+        mark.appendChild(el('span', `${PREFIX}success-line-long`));
         icon.appendChild(mark);
-        icon.appendChild(ring);
-        icon.appendChild(fix);
-        icon.appendChild(right);
+        icon.appendChild(el('div', `${PREFIX}success-ring`));
+        icon.appendChild(el('div', `${PREFIX}success-fix`));
+        icon.appendChild(el('div', `${PREFIX}success-circular-line-right`));
 
         applyIconSize('font');
         return icon;
@@ -572,68 +353,13 @@
 
       if (type === 'error' || type === 'warning' || type === 'info' || type === 'question') {
         // Use the same "success-like" ring parts for every icon
-        const left = document.createElement('div');
-        left.className = `${PREFIX}success-circular-line-left`;
-        const ring = document.createElement('div');
-        ring.className = `${PREFIX}success-ring`;
-        const fix = document.createElement('div');
-        fix.className = `${PREFIX}success-fix`;
-        const right = document.createElement('div');
-        right.className = `${PREFIX}success-circular-line-right`;
-        icon.appendChild(left);
-        icon.appendChild(ring);
-        icon.appendChild(fix);
-        icon.appendChild(right);
+        appendRingParts();
 
         applyIconSize('font');
 
         // SVG only draws the inner symbol (no SVG ring)
-        const svg = document.createElementNS(svgNs, 'svg');
-        svg.setAttribute('viewBox', '0 0 80 80');
-        svg.setAttribute('aria-hidden', 'true');
-        svg.setAttribute('focusable', 'false');
-
-        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 === 'error') {
-          addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
-          addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
-        } else if (type === 'warning') {
-          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 if (type === 'question') {
-          addPath('M30 30 C30 23 35 19 42 19 C49 19 54 23 54 30 C54 36 50 39 46 41 C43 42 42 44 42 48 L42 52', `${PREFIX}svg-question`);
-          const dot = document.createElementNS(svgNs, 'circle');
-          dot.setAttribute('class', `${PREFIX}svg-dot`);
-          dot.setAttribute('cx', '42');
-          dot.setAttribute('cy', '61');
-          dot.setAttribute('r', '3.2');
-          dot.setAttribute('fill', 'currentColor');
-          svg.appendChild(dot);
-        }
-
+        const svg = createInnerMarkSvg();
+        appendBuiltInInnerMark(svg);
         icon.appendChild(svg);
         return icon;
       }
@@ -641,12 +367,9 @@
       // Default to SVG icons for other/custom types
       applyIconSize('box');
 
-      const svg = document.createElementNS(svgNs, 'svg');
-      svg.setAttribute('viewBox', '0 0 80 80');
-      svg.setAttribute('aria-hidden', 'true');
-      svg.setAttribute('focusable', 'false');
+      const svg = createInnerMarkSvg();
 
-      const ring = document.createElementNS(svgNs, 'circle');
+      const ring = svgEl('circle');
       ring.setAttribute('class', `${PREFIX}svg-ring ${PREFIX}svg-draw`);
       ring.setAttribute('cx', '40');
       ring.setAttribute('cy', '40');
@@ -654,50 +377,7 @@
       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 === 'error') {
-        addPath('M28 28 L52 52', `${PREFIX}svg-error-left`);
-        addPath('M52 28 L28 52', `${PREFIX}svg-error-right`);
-      } else if (type === 'warning') {
-        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 if (type === 'question') {
-        // Question mark (single stroke + dot)
-        addPath('M30 30 C30 23 35 19 42 19 C49 19 54 23 54 30 C54 36 50 39 46 41 C43 42 42 44 42 48 L42 52', `${PREFIX}svg-question`);
-        const dot = document.createElementNS(svgNs, 'circle');
-        dot.setAttribute('class', `${PREFIX}svg-dot`);
-        dot.setAttribute('cx', '42');
-        dot.setAttribute('cy', '61');
-        dot.setAttribute('r', '3.2');
-        dot.setAttribute('fill', 'currentColor');
-        svg.appendChild(dot);
-      } else {
-        // Fallback: ring only
-      }
+      // For custom types, we draw ring only by default (no inner mark).
 
       icon.appendChild(svg);
       
@@ -710,7 +390,7 @@
         const popup = this.dom && this.dom.popup;
         if (!icon || !popup) return;
         const bg = getComputedStyle(popup).backgroundColor;
-        const parts = icon.querySelectorAll(`.${PREFIX}success-circular-line-left, .${PREFIX}success-circular-line-right, .${PREFIX}success-fix`);
+        const parts = icon.querySelectorAll(RING_BG_PARTS_SELECTOR);
         parts.forEach((el) => {
           try { el.style.backgroundColor = bg; } catch {}
         });
@@ -764,10 +444,8 @@
       if (!icon) return;
       const type = (this.params && this.params.icon) || '';
 
-      const ringTypes = new Set(['success', 'error', 'warning', 'info', 'question']);
-
       // Ring animation (same as success) for all built-in icons
-      if (ringTypes.has(type)) this._adjustRingBackgroundColor();
+      if (RING_TYPES.has(type)) this._adjustRingBackgroundColor();
       try { icon.classList.remove(`${PREFIX}icon-show`); } catch {}
       requestAnimationFrame(() => {
         try { icon.classList.add(`${PREFIX}icon-show`); } catch {}
@@ -787,7 +465,7 @@
 
       // If this is a built-in icon, keep the SweetAlert-like order:
       // ring sweep first (~0.51s), then draw the inner mark.
-      const baseDelay = ringTypes.has(type) ? 520 : 0;
+      const baseDelay = RING_TYPES.has(type) ? 520 : 0;
 
       if (type === 'error') {
         // Draw order: left-top -> right-bottom, then right-top -> left-bottom

+ 286 - 0
xjs.css

@@ -0,0 +1,286 @@
+/* XJS library styles
+ * This file centralizes all runtime UI CSS used by the library.
+ * Currently includes: Layer popup (layer-*)
+ */
+
+/* =========================
+   Layer (layer-*)
+   ========================= */
+
+.layer-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+  opacity: 0;
+  transition: opacity 0.3s;
+}
+.layer-overlay.show {
+  opacity: 1;
+}
+.layer-popup {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  width: 32em;
+  max-width: 90%;
+  padding: 1.5em;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+  z-index: 1;
+  transform: scale(0.92);
+  transition: transform 0.26s ease;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+.layer-overlay.show .layer-popup {
+  transform: scale(1);
+}
+.layer-title {
+  font-size: 1.8em;
+  font-weight: 600;
+  color: #333;
+  margin: 0 0 0.5em;
+  text-align: center;
+}
+.layer-content {
+  font-size: 1.125em;
+  color: #545454;
+  margin-bottom: 1.5em;
+  text-align: center;
+  line-height: 1.5;
+}
+.layer-actions {
+  display: flex;
+  justify-content: center;
+  gap: 1em;
+  width: 100%;
+}
+.layer-button {
+  border: none;
+  border-radius: 4px;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  cursor: pointer;
+  transition: background-color 0.2s, box-shadow 0.2s;
+  color: #fff;
+}
+.layer-confirm {
+  background-color: #3085d6;
+}
+.layer-confirm:hover {
+  background-color: #2b77c0;
+}
+.layer-cancel {
+  background-color: #aaa;
+}
+.layer-cancel:hover {
+  background-color: #999;
+}
+.layer-icon {
+  position: relative;
+  box-sizing: content-box;
+  width: 5em;
+  height: 5em;
+  margin: 0.5em auto 1.8em;
+  border: 0.25em solid rgba(0, 0, 0, 0);
+  border-radius: 50%;
+  font-family: inherit;
+  line-height: 5em;
+  cursor: default;
+  user-select: none;
+  /* Ring sweep angles (match SweetAlert2 success feel) */
+  --layer-ring-start-rotate: -45deg;
+  /* End is one full turn from start so it "sweeps then settles" */
+  --layer-ring-end-rotate: -405deg;
+}
+
+/* SVG mark content (kept), sits above the ring */
+.layer-icon svg {
+  width: 100%;
+  height: 100%;
+  display: block;
+  overflow: visible;
+  position: relative;
+  z-index: 3;
+}
+.layer-svg-ring,
+.layer-svg-mark {
+  fill: none;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+.layer-svg-ring {
+  stroke-width: 4.5;
+  opacity: 0.95;
+}
+.layer-svg-mark {
+  stroke-width: 6;
+}
+.layer-svg-dot {
+  transform-box: fill-box;
+  transform-origin: center;
+}
+
+/* =========================================
+   "success-like" ring (shared by all icons)
+   - we reuse the success ring pieces/rotation for every icon type
+   - color is driven by currentColor
+   ========================================= */
+.layer-icon.success { border-color: #a5dc86; color: #a5dc86; }
+.layer-icon.error { border-color: #f27474; color: #f27474; }
+.layer-icon.warning { border-color: #f8bb86; color: #f8bb86; }
+.layer-icon.info { border-color: #3fc3ee; color: #3fc3ee; }
+.layer-icon.question { border-color: #b18cff; color: #b18cff; }
+
+/* Keep ring sweep logic identical across built-in icons (match success). */
+.layer-icon .layer-success-ring {
+  position: absolute;
+  z-index: 2;
+  top: -0.25em;
+  left: -0.25em;
+  box-sizing: content-box;
+  width: 100%;
+  height: 100%;
+  border: 0.25em solid currentColor;
+  opacity: 0.3;
+  border-radius: 50%;
+}
+.layer-icon .layer-success-fix {
+  position: absolute;
+  z-index: 1;
+  top: 0.5em;
+  left: 1.625em;
+  width: 0.4375em;
+  height: 5.625em;
+  transform: rotate(-45deg);
+  background-color: #fff; /* adjusted at runtime to popup bg */
+}
+.layer-icon .layer-success-circular-line-left,
+.layer-icon .layer-success-circular-line-right {
+  position: absolute;
+  width: 3.75em;
+  height: 7.5em;
+  border-radius: 50%;
+  background-color: #fff; /* adjusted at runtime to popup bg */
+}
+.layer-icon .layer-success-circular-line-left {
+  top: -0.4375em;
+  left: -2.0635em;
+  transform: rotate(-45deg);
+  transform-origin: 3.75em 3.75em;
+  border-radius: 7.5em 0 0 7.5em;
+}
+.layer-icon .layer-success-circular-line-right {
+  top: -0.6875em;
+  left: 1.875em;
+  transform: rotate(-45deg);
+  transform-origin: 0 3.75em;
+  border-radius: 0 7.5em 7.5em 0;
+}
+/* Clip ONLY the success tick (do not clip the ring sweep masks) */
+.layer-icon .layer-success-mark {
+  position: absolute;
+  inset: 0;
+  border-radius: 50%;
+  overflow: hidden;
+  z-index: 3;
+  pointer-events: none;
+}
+.layer-icon .layer-success-line-tip,
+.layer-icon .layer-success-line-long {
+  display: block;
+  position: absolute;
+  z-index: 2;
+  height: 0.3125em;
+  border-radius: 0.125em;
+  background-color: currentColor;
+}
+.layer-icon .layer-success-line-tip {
+  top: 2.875em;
+  left: 0.8125em;
+  width: 1.5625em;
+  transform: rotate(45deg);
+}
+.layer-icon .layer-success-line-long {
+  top: 2.375em;
+  right: 0.5em;
+  width: 2.9375em;
+  transform: rotate(-45deg);
+}
+
+/* Triggered when icon has .layer-icon-show */
+.layer-icon.layer-icon-show .layer-success-line-tip {
+  animation: layer-animate-success-line-tip 0.75s;
+}
+.layer-icon.layer-icon-show .layer-success-line-long {
+  animation: layer-animate-success-line-long 0.75s;
+}
+.layer-icon.layer-icon-show .layer-success-circular-line-right {
+  animation: layer-rotate-icon-circular-line 4.25s ease-in;
+}
+
+@keyframes layer-animate-success-line-tip {
+  0% {
+    top: 1.1875em;
+    left: 0.0625em;
+    width: 0;
+  }
+  54% {
+    top: 1.0625em;
+    left: 0.125em;
+    width: 0;
+  }
+  70% {
+    top: 2.1875em;
+    left: -0.375em;
+    width: 3.125em;
+  }
+  84% {
+    top: 3em;
+    left: 1.3125em;
+    width: 1.0625em;
+  }
+  100% {
+    top: 2.8125em;
+    left: 0.8125em;
+    width: 1.5625em;
+  }
+}
+@keyframes layer-animate-success-line-long {
+  0% {
+    top: 3.375em;
+    right: 2.875em;
+    width: 0;
+  }
+  65% {
+    top: 3.375em;
+    right: 2.875em;
+    width: 0;
+  }
+  84% {
+    top: 2.1875em;
+    right: 0;
+    width: 3.4375em;
+  }
+  100% {
+    top: 2.375em;
+    right: 0.5em;
+    width: 2.9375em;
+  }
+}
+@keyframes layer-rotate-icon-circular-line {
+  0% { transform: rotate(var(--layer-ring-start-rotate)); }
+  5% { transform: rotate(var(--layer-ring-start-rotate)); }
+  12% { transform: rotate(var(--layer-ring-end-rotate)); }
+  100% { transform: rotate(var(--layer-ring-end-rotate)); } /* match 12% so it "sweeps then stops" */
+}
+
+/* (Other icon shapes stay SVG-driven; only the ring animation is unified above) */

+ 37 - 279
xjs.js

@@ -105,291 +105,49 @@
 
   const PREFIX = 'layer-';
   
-  // Theme & Styles
-  const css = `
-    .${PREFIX}overlay {
-      position: fixed;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-      background: rgba(0, 0, 0, 0.4);
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      z-index: 1000;
-      opacity: 0;
-      transition: opacity 0.3s;
-    }
-    .${PREFIX}overlay.show {
-      opacity: 1;
-    }
-    .${PREFIX}popup {
-      background: #fff;
-      border-radius: 8px;
-      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
-      width: 32em;
-      max-width: 90%;
-      padding: 1.5em;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      position: relative;
-      z-index: 1;
-      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 {
-      transform: scale(1);
-    }
-    .${PREFIX}title {
-      font-size: 1.8em;
-      font-weight: 600;
-      color: #333;
-      margin: 0 0 0.5em;
-      text-align: center;
-    }
-    .${PREFIX}content {
-      font-size: 1.125em;
-      color: #545454;
-      margin-bottom: 1.5em;
-      text-align: center;
-      line-height: 1.5;
-    }
-    .${PREFIX}actions {
-      display: flex;
-      justify-content: center;
-      gap: 1em;
-      width: 100%;
-    }
-    .${PREFIX}button {
-      border: none;
-      border-radius: 4px;
-      padding: 0.6em 1.2em;
-      font-size: 1em;
-      cursor: pointer;
-      transition: background-color 0.2s, box-shadow 0.2s;
-      color: #fff;
-    }
-    .${PREFIX}confirm {
-      background-color: #3085d6;
-    }
-    .${PREFIX}confirm:hover {
-      background-color: #2b77c0;
-    }
-    .${PREFIX}cancel {
-      background-color: #aaa;
-    }
-    .${PREFIX}cancel:hover {
-      background-color: #999;
-    }
-    .${PREFIX}icon {
-      position: relative;
-      box-sizing: content-box;
-      width: 5em;
-      height: 5em;
-      margin: 0.5em auto 1.8em;
-      border: 0.25em solid rgba(0, 0, 0, 0);
-      border-radius: 50%;
-      font-family: inherit;
-      line-height: 5em;
-      cursor: default;
-      user-select: none;
-      /* Ring sweep angles (match SweetAlert2 success feel) */
-      --${PREFIX}ring-start-rotate: -45deg;
-      /* End is one full turn from start so it "sweeps then settles" */
-      --${PREFIX}ring-end-rotate: -405deg;
-    }
-
-    /* SVG mark content (kept), sits above the ring */
-    .${PREFIX}icon svg {
-      width: 100%;
-      height: 100%;
-      display: block;
-      overflow: visible;
-      position: relative;
-      z-index: 3;
-    }
-    .${PREFIX}svg-ring,
-    .${PREFIX}svg-mark {
-      fill: none;
-      stroke-linecap: round;
-      stroke-linejoin: round;
-    }
-    .${PREFIX}svg-ring {
-      stroke-width: 4.5;
-      opacity: 0.95;
-    }
-    .${PREFIX}svg-mark {
-      stroke-width: 6;
-    }
-    .${PREFIX}svg-dot {
-      transform-box: fill-box;
-      transform-origin: center;
-    }
-
-    /* =========================================
-       "success-like" ring (shared by all icons)
-       - we reuse the success ring pieces/rotation for every icon type
-       - color is driven by currentColor
-       ========================================= */
-    .${PREFIX}icon.success { border-color: #a5dc86; color: #a5dc86; }
-    .${PREFIX}icon.error { border-color: #f27474; color: #f27474; }
-    .${PREFIX}icon.warning { border-color: #f8bb86; color: #f8bb86; }
-    .${PREFIX}icon.info { border-color: #3fc3ee; color: #3fc3ee; }
-    .${PREFIX}icon.question { border-color: #b18cff; color: #b18cff; }
-
-    /* Keep ring sweep logic identical across built-in icons (match success). */
-
-    .${PREFIX}icon .${PREFIX}success-ring {
-      position: absolute;
-      z-index: 2;
-      top: -0.25em;
-      left: -0.25em;
-      box-sizing: content-box;
-      width: 100%;
-      height: 100%;
-      border: 0.25em solid currentColor;
-      opacity: 0.3;
-      border-radius: 50%;
-    }
-    .${PREFIX}icon .${PREFIX}success-fix {
-      position: absolute;
-      z-index: 1;
-      top: 0.5em;
-      left: 1.625em;
-      width: 0.4375em;
-      height: 5.625em;
-      transform: rotate(-45deg);
-      background-color: #fff; /* adjusted at runtime to popup bg */
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-left,
-    .${PREFIX}icon .${PREFIX}success-circular-line-right {
-      position: absolute;
-      width: 3.75em;
-      height: 7.5em;
-      border-radius: 50%;
-      background-color: #fff; /* adjusted at runtime to popup bg */
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-left {
-      top: -0.4375em;
-      left: -2.0635em;
-      transform: rotate(-45deg);
-      transform-origin: 3.75em 3.75em;
-      border-radius: 7.5em 0 0 7.5em;
-    }
-    .${PREFIX}icon .${PREFIX}success-circular-line-right {
-      top: -0.6875em;
-      left: 1.875em;
-      transform: rotate(-45deg);
-      transform-origin: 0 3.75em;
-      border-radius: 0 7.5em 7.5em 0;
-    }
-    .${PREFIX}icon .${PREFIX}success-line-tip,
-    .${PREFIX}icon .${PREFIX}success-line-long {
-      display: block;
-      position: absolute;
-      z-index: 2;
-      height: 0.3125em;
-      border-radius: 0.125em;
-      background-color: currentColor;
-    }
-    .${PREFIX}icon .${PREFIX}success-line-tip {
-      top: 2.875em;
-      left: 0.8125em;
-      width: 1.5625em;
-      transform: rotate(45deg);
-    }
-    .${PREFIX}icon .${PREFIX}success-line-long {
-      top: 2.375em;
-      right: 0.5em;
-      width: 2.9375em;
-      transform: rotate(-45deg);
-    }
-
-    /* Triggered when icon has .${PREFIX}icon-show */
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-tip {
-      animation: ${PREFIX}animate-success-line-tip 0.75s;
-    }
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-line-long {
-      animation: ${PREFIX}animate-success-line-long 0.75s;
-    }
-    .${PREFIX}icon.${PREFIX}icon-show .${PREFIX}success-circular-line-right {
-      animation: ${PREFIX}rotate-icon-circular-line 4.25s ease-in;
-    }
-
-    @keyframes ${PREFIX}animate-success-line-tip {
-      0% {
-        top: 1.1875em;
-        left: 0.0625em;
-        width: 0;
-      }
-      54% {
-        top: 1.0625em;
-        left: 0.125em;
-        width: 0;
-      }
-      70% {
-        top: 2.1875em;
-        left: -0.375em;
-        width: 3.125em;
-      }
-      84% {
-        top: 3em;
-        left: 1.3125em;
-        width: 1.0625em;
-      }
-      100% {
-        top: 2.8125em;
-        left: 0.8125em;
-        width: 1.5625em;
-      }
-    }
-    @keyframes ${PREFIX}animate-success-line-long {
-      0% {
-        top: 3.375em;
-        right: 2.875em;
-        width: 0;
-      }
-      65% {
-        top: 3.375em;
-        right: 2.875em;
-        width: 0;
-      }
-      84% {
-        top: 2.1875em;
-        right: 0;
-        width: 3.4375em;
-      }
-      100% {
-        top: 2.375em;
-        right: 0.5em;
-        width: 2.9375em;
+  // Ensure library CSS is loaded (xjs.css)
+  // - CSS is centralized in xjs.css (no runtime <style> injection).
+  // - We still auto-load it for convenience/compat, since consumers may forget the <link>.
+  const ensureXjsCss = () => {
+    try {
+      if (typeof document === 'undefined') return;
+      if (!document.head) return;
+
+      // If any stylesheet already points to xjs.css, do nothing.
+      const existingHref = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
+        .map((l) => (l.getAttribute('href') || '').trim())
+        .find((h) => /(^|\/)xjs\.css(\?|#|$)/.test(h));
+      if (existingHref) return;
+
+      const id = 'xjs-css';
+      if (document.getElementById(id)) return;
+
+      // Try to derive xjs.css from a loaded script URL (xjs.js or layer.js).
+      const scripts = Array.from(document.getElementsByTagName('script'));
+      const scriptSrc = scripts
+        .map((s) => s && s.src)
+        .find((src) => /(^|\/)(xjs|layer)\.js(\?|#|$)/.test(String(src || '')));
+
+      let href = 'xjs.css';
+      if (scriptSrc) {
+        href = String(scriptSrc)
+          .replace(/(^|\/)xjs\.js(\?|#|$)/, '$1xjs.css$2')
+          .replace(/(^|\/)layer\.js(\?|#|$)/, '$1xjs.css$2');
       }
-    }
-    @keyframes ${PREFIX}rotate-icon-circular-line {
-      0% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
-      5% { transform: rotate(var(--${PREFIX}ring-start-rotate)); }
-      12% { transform: rotate(var(--${PREFIX}ring-end-rotate)); }
-      100% { transform: rotate(var(--${PREFIX}ring-end-rotate)); } /* match 12% so it "sweeps then stops" */
-    }
-
-    /* (Other icon shapes stay SVG-driven; only the ring animation is unified above) */
-  `;
 
-  // Inject Styles
-  const injectStyles = () => {
-    if (document.getElementById(`${PREFIX}styles`)) return;
-    const styleSheet = document.createElement('style');
-    styleSheet.id = `${PREFIX}styles`;
-    styleSheet.textContent = css;
-    document.head.appendChild(styleSheet);
+      const link = document.createElement('link');
+      link.id = id;
+      link.rel = 'stylesheet';
+      link.href = href;
+      document.head.appendChild(link);
+    } catch {
+      // ignore
+    }
   };
 
   class Layer {
     constructor() {
-      injectStyles();
+      ensureXjsCss();
       this.params = {};
       this.dom = {};
       this.promise = null;