Compare commits
No commits in common. "main" and "v1.0.4" have entirely different histories.
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,22 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.6 (2026-06-09)
|
||||
|
||||
### Breaking Changes
|
||||
- **ESM Deprecation**: Completely removed ESM exports to align with the "zero-boilerplate" project philosophy. The library now relies solely on global injection via `globalThis`.
|
||||
- **Build Format**: Switched build output to IIFE format only.
|
||||
|
||||
### Improvements
|
||||
- **Integration**: Simplified dependency handling for `@apigo.cc/state` by using global variables.
|
||||
- **Docs**: Updated README with new versioning and non-ESM usage instructions.
|
||||
|
||||
## 1.0.5 (2026-06-05)
|
||||
|
||||
### Features
|
||||
- **API**: Refactored `Bootstrap.config` to be the unified entry point.
|
||||
- **Shorthand**: Added support for `[state, key]` array shorthand in `bindDarkMode`.
|
||||
- **Theme Engine**: Expanded CSS patch coverage to include form switches, checkboxes, range inputs, progress bars, and more.
|
||||
- **AI Optimized**: Documentation overhaul for better AI usability and zero-config setup.
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where theme colors did not propagate to component-specific variables like buttons and switches.
|
||||
88
README.md
88
README.md
@ -1,48 +1,68 @@
|
||||
# @apigo.cc/bootstrap API 手册 (AI Optimized)
|
||||
# @apigo.cc/bootstrap
|
||||
|
||||
Bootstrap 5.3 自包含集成引擎。
|
||||
`@apigo.cc/bootstrap` 是一个自包含的 Bootstrap 5.3 引擎集成模块。它内置了 Bootstrap 及其图标库(Bootstrap Icons),并提供了简单的主题配色与暗色模式绑定机制。
|
||||
|
||||
---
|
||||
|
||||
## 一、 集成方式
|
||||
|
||||
推荐使用 `@apigo.cc/loader` 自动调度:
|
||||
|
||||
## 0. 快速开始 (Quick Start)
|
||||
直接在 HTML 中引入(无需打包,完全非 ESM 注入):
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/bootstrap@1.0.6/dist/bootstrap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/loader@1.0.0/dist/loader.min.js"></script>
|
||||
<script>
|
||||
Loader.load('state:1.0.11', 'bootstrap:1.0.2');
|
||||
</script>
|
||||
```
|
||||
|
||||
## 1. 运行时接口
|
||||
- **`bootstrap`**: (Object) 官方原生 Bootstrap 镜像。已挂载至 `globalThis.bootstrap`。
|
||||
- **`Bootstrap`**: (Object) 增强控制接口。已挂载至 `globalThis.Bootstrap`。
|
||||
---
|
||||
|
||||
*(注意:本库自 v1.0.6 起全面废弃 ESM 导出,改为纯全局注入模式。)*
|
||||
## 二、 核心 API
|
||||
|
||||
## 2. API 参考
|
||||
### 1. 修改主题配色 (`Bootstrap.config`)
|
||||
|
||||
### `Bootstrap.config(options)`
|
||||
统一配置入口。未定义的 key 将被忽略。
|
||||
您可以通过调用 `Bootstrap.config(colors)` 方法动态修改 Bootstrap 的核心主题颜色。传入十六进制(Hex)值,方法会自动为您计算并设置相应的 CSS 变量以及 RGB 变量:
|
||||
|
||||
#### 参数 `options`:
|
||||
- **主题色**: `primary`, `secondary`, `success`, `info`, `warning`, `danger`, `light`, `dark` (Hex 字符串)。
|
||||
- *行为*: 自动更新 CSS 变量并注入深度样式补丁(覆盖按钮、表单控件、开关、进度条等硬编码样式)。
|
||||
- **暗黑模式**:
|
||||
- `darkMode`: (Boolean) 直接设置主题。`true` 为 dark,`false` 为 light。
|
||||
- `bindDarkMode`: (Array) `[state, key]`。绑定响应式状态。
|
||||
- `state`: 具有 `__watch(key, callback)` 方法的状态对象。
|
||||
- `key`: 状态对象中的键名。
|
||||
|
||||
#### 示例 (Examples):
|
||||
```javascript
|
||||
// 1. 设置颜色与暗黑模式简写
|
||||
import { Bootstrap } from '@apigo.cc/bootstrap';
|
||||
|
||||
// 动态修改 primary 和 danger 的配色
|
||||
Bootstrap.config({
|
||||
primary: '#a855f7',
|
||||
bindDarkMode: [LocalStorage, 'isDark']
|
||||
primary: '#6366f1',
|
||||
danger: '#f43f5e'
|
||||
});
|
||||
|
||||
// 2. 手动模式
|
||||
Bootstrap.config({ darkMode: true });
|
||||
|
||||
// 3. 仅更新单一颜色 (自动处理相关组件补丁)
|
||||
Bootstrap.config({ success: '#22c55e' });
|
||||
```
|
||||
|
||||
## 3. 核心机制 (Internal)
|
||||
- **运行时样式补丁**: `Bootstrap.config` 动态更新 `id="bs-config-patch"` 的 `<style>` 标签。通过 `!important` 覆盖 Bootstrap 内部硬编码的组件颜色(如 `.btn`, `.form-switch`, `.form-range`, `.progress-bar`, `.list-group-item` 等)。
|
||||
- **零构建支持**: 无需 Sass 重新编译,在纯 ESM 环境下即可实现全量主题定制。
|
||||
### 2. 绑定暗色模式 (`Bootstrap.bindDarkMode`)
|
||||
|
||||
`Bootstrap.bindDarkMode(state, key)` 可以将响应式状态(如 `LocalStorage` 或您自定义的 `NewState`)的某个属性绑定为暗色模式的控制器。
|
||||
|
||||
当该属性发生变化时,它会自动在 `<html>` 标签上设置 `data-bs-theme="dark"` 或 `data-bs-theme="light"`,从而无缝切换 Bootstrap 5.3 的深色主题模式:
|
||||
|
||||
```javascript
|
||||
import { LocalStorage } from '@apigo.cc/state';
|
||||
import { Bootstrap } from '@apigo.cc/bootstrap';
|
||||
|
||||
// 绑定 LocalStorage.darkMode 状态
|
||||
Bootstrap.bindDarkMode(LocalStorage, 'darkMode');
|
||||
|
||||
// 示例:切换为暗色模式
|
||||
LocalStorage.darkMode = true;
|
||||
|
||||
// 示例:切换回亮色模式
|
||||
LocalStorage.darkMode = false;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、 使用原生 Bootstrap 实例
|
||||
|
||||
`@apigo.cc/bootstrap` 导出了原生的 Bootstrap 全量实例,您可以直接调用原生 Bootstrap 的组件方法(如 Modal, Popover 等):
|
||||
|
||||
```javascript
|
||||
import { Bootstrap } from '@apigo.cc/bootstrap';
|
||||
|
||||
// 使用原生的 Modal 组件
|
||||
const myModal = new Bootstrap.Modal(document.getElementById('myModal'));
|
||||
myModal.show();
|
||||
```
|
||||
|
||||
183
dist/bootstrap.js
vendored
183
dist/bootstrap.js
vendored
@ -2120,8 +2120,7 @@ url("data:font/woff;base64,d09GRgABAAAAAsBAAAsAAAAHavgAAQAAAAAAAAAAAAAAAAAAAAAAA
|
||||
console.error("vite-plugin-css-injected-by-js", e);
|
||||
}
|
||||
})();
|
||||
var ApigoBootstrap = function(exports) {
|
||||
"use strict";
|
||||
import "@web/state";
|
||||
var top = "top";
|
||||
var bottom = "bottom";
|
||||
var right = "right";
|
||||
@ -3299,14 +3298,14 @@ var ApigoBootstrap = function(exports) {
|
||||
}));
|
||||
}, []);
|
||||
}
|
||||
function debounce(fn) {
|
||||
function debounce(fn2) {
|
||||
var pending;
|
||||
return function() {
|
||||
if (!pending) {
|
||||
pending = new Promise(function(resolve) {
|
||||
Promise.resolve().then(function() {
|
||||
pending = void 0;
|
||||
resolve(fn());
|
||||
resolve(fn2());
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -3407,9 +3406,9 @@ var ApigoBootstrap = function(exports) {
|
||||
index = -1;
|
||||
continue;
|
||||
}
|
||||
var _state$orderedModifie = state.orderedModifiers[index], fn = _state$orderedModifie.fn, _state$orderedModifie2 = _state$orderedModifie.options, _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, name = _state$orderedModifie.name;
|
||||
if (typeof fn === "function") {
|
||||
state = fn({
|
||||
var _state$orderedModifie = state.orderedModifiers[index], fn2 = _state$orderedModifie.fn, _state$orderedModifie2 = _state$orderedModifie.options, _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, name = _state$orderedModifie.name;
|
||||
if (typeof fn2 === "function") {
|
||||
state = fn2({
|
||||
state,
|
||||
options: _options,
|
||||
name,
|
||||
@ -3456,8 +3455,8 @@ var ApigoBootstrap = function(exports) {
|
||||
});
|
||||
}
|
||||
function cleanupModifierEffects() {
|
||||
effectCleanupFns.forEach(function(fn) {
|
||||
return fn();
|
||||
effectCleanupFns.forEach(function(fn2) {
|
||||
return fn2();
|
||||
});
|
||||
effectCleanupFns = [];
|
||||
}
|
||||
@ -3750,18 +3749,18 @@ var ApigoBootstrap = function(exports) {
|
||||
eventRegistry[uid] = eventRegistry[uid] || {};
|
||||
return eventRegistry[uid];
|
||||
}
|
||||
function bootstrapHandler(element, fn) {
|
||||
function bootstrapHandler(element, fn2) {
|
||||
return function handler(event) {
|
||||
hydrateObj(event, {
|
||||
delegateTarget: element
|
||||
});
|
||||
if (handler.oneOff) {
|
||||
EventHandler.off(element, event.type, fn);
|
||||
EventHandler.off(element, event.type, fn2);
|
||||
}
|
||||
return fn.apply(element, [event]);
|
||||
return fn2.apply(element, [event]);
|
||||
};
|
||||
}
|
||||
function bootstrapDelegationHandler(element, selector, fn) {
|
||||
function bootstrapDelegationHandler(element, selector, fn2) {
|
||||
return function handler(event) {
|
||||
const domElements = element.querySelectorAll(selector);
|
||||
for (let {
|
||||
@ -3775,9 +3774,9 @@ var ApigoBootstrap = function(exports) {
|
||||
delegateTarget: target
|
||||
});
|
||||
if (handler.oneOff) {
|
||||
EventHandler.off(element, event.type, selector, fn);
|
||||
EventHandler.off(element, event.type, selector, fn2);
|
||||
}
|
||||
return fn.apply(target, [event]);
|
||||
return fn2.apply(target, [event]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -3800,10 +3799,10 @@ var ApigoBootstrap = function(exports) {
|
||||
}
|
||||
let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);
|
||||
if (originalTypeEvent in customEvents) {
|
||||
const wrapFunction = (fn2) => {
|
||||
const wrapFunction = (fn3) => {
|
||||
return function(event) {
|
||||
if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {
|
||||
return fn2.call(this, event);
|
||||
return fn3.call(this, event);
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -3817,21 +3816,21 @@ var ApigoBootstrap = function(exports) {
|
||||
return;
|
||||
}
|
||||
const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ""));
|
||||
const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);
|
||||
fn.delegationSelector = isDelegated ? handler : null;
|
||||
fn.callable = callable;
|
||||
fn.oneOff = oneOff;
|
||||
fn.uidEvent = uid;
|
||||
handlers[uid] = fn;
|
||||
element.addEventListener(typeEvent, fn, isDelegated);
|
||||
const fn2 = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);
|
||||
fn2.delegationSelector = isDelegated ? handler : null;
|
||||
fn2.callable = callable;
|
||||
fn2.oneOff = oneOff;
|
||||
fn2.uidEvent = uid;
|
||||
handlers[uid] = fn2;
|
||||
element.addEventListener(typeEvent, fn2, isDelegated);
|
||||
}
|
||||
function removeHandler(element, events, typeEvent, handler, delegationSelector) {
|
||||
const fn = findHandler(events[typeEvent], handler, delegationSelector);
|
||||
if (!fn) {
|
||||
const fn2 = findHandler(events[typeEvent], handler, delegationSelector);
|
||||
if (!fn2) {
|
||||
return;
|
||||
}
|
||||
element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));
|
||||
delete events[typeEvent][fn.uidEvent];
|
||||
element.removeEventListener(typeEvent, fn2, Boolean(delegationSelector));
|
||||
delete events[typeEvent][fn2.uidEvent];
|
||||
}
|
||||
function removeNamespacedHandlers(element, events, typeEvent, namespace) {
|
||||
const storeElementEvent = events[typeEvent] || {};
|
||||
@ -7178,107 +7177,51 @@ var ApigoBootstrap = function(exports) {
|
||||
Tooltip
|
||||
}, Symbol.toStringTag, { value: "Module" }));
|
||||
const Bootstrap = {
|
||||
config: (options = {}) => {
|
||||
// 原始 Bootstrap 实例引用
|
||||
...bootstrap,
|
||||
/**
|
||||
* 绑定暗色模式
|
||||
* @param {Object} state NewState 对象 (如 LocalStorage)
|
||||
* @param {string} key 键名 (如 'darkMode')
|
||||
*/
|
||||
bindDarkMode: (state, key) => {
|
||||
if (typeof document === "undefined") return;
|
||||
const root = document.documentElement;
|
||||
const colors = ["primary", "secondary", "success", "info", "warning", "danger", "light", "dark"];
|
||||
let cssPatch = "";
|
||||
colors.forEach((name) => {
|
||||
const hex = options[name];
|
||||
if (hex) {
|
||||
root.style.setProperty(`--bs-${name}`, hex);
|
||||
const rgb = Bootstrap._hexToRgb(hex);
|
||||
if (rgb) {
|
||||
const rgbStr = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
|
||||
root.style.setProperty(`--bs-${name}-rgb`, rgbStr);
|
||||
cssPatch += `
|
||||
.btn-${name} {
|
||||
--bs-btn-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-border-color: var(--bs-${name}) !important;
|
||||
--bs-btn-hover-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-hover-border-color: var(--bs-${name}) !important;
|
||||
--bs-btn-active-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-active-border-color: var(--bs-${name}) !important;
|
||||
--bs-btn-disabled-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-disabled-border-color: var(--bs-${name}) !important;
|
||||
}
|
||||
.bg-${name} { background-color: var(--bs-${name}) !important; }
|
||||
.text-${name} { color: var(--bs-${name}) !important; }
|
||||
.border-${name} { border-color: var(--bs-${name}) !important; }
|
||||
.badge.bg-${name} { background-color: var(--bs-${name}) !important; }
|
||||
|
||||
/* 表单控件补丁 (Form Controls) */
|
||||
.form-check-input:checked {
|
||||
background-color: var(--bs-primary) !important;
|
||||
border-color: var(--bs-primary) !important;
|
||||
}
|
||||
.form-check-input:focus {
|
||||
border-color: var(--bs-primary) !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25) !important;
|
||||
}
|
||||
.form-switch .form-check-input:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e") !important;
|
||||
}
|
||||
.form-range::-webkit-slider-thumb { background: var(--bs-primary) !important; }
|
||||
.form-range::-moz-range-thumb { background: var(--bs-primary) !important; }
|
||||
.form-range::-webkit-slider-thumb:active { background-color: rgba(var(--bs-primary-rgb), 0.5) !important; }
|
||||
|
||||
/* 进度条与分页 (Progress & Pagination) */
|
||||
.progress-bar { background-color: var(--bs-primary) !important; }
|
||||
.page-link { color: var(--bs-primary); }
|
||||
.active > .page-link, .page-link.active {
|
||||
background-color: var(--bs-primary) !important;
|
||||
border-color: var(--bs-primary) !important;
|
||||
}
|
||||
|
||||
/* 列表组 (List Group) */
|
||||
.list-group-item.active {
|
||||
background-color: var(--bs-primary) !important;
|
||||
border-color: var(--bs-primary) !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (cssPatch) {
|
||||
let styleEl = document.getElementById("bs-config-patch");
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement("style");
|
||||
styleEl.id = "bs-config-patch";
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
styleEl.innerHTML = cssPatch;
|
||||
}
|
||||
let { bindDarkMode, darkMode } = options;
|
||||
const htmlNode = document.documentElement;
|
||||
const updateTheme = (val) => {
|
||||
root.setAttribute("data-bs-theme", val ? "dark" : "light");
|
||||
htmlNode.setAttribute("data-bs-theme", val ? "dark" : "light");
|
||||
};
|
||||
if (Array.isArray(bindDarkMode)) {
|
||||
bindDarkMode = { state: bindDarkMode[0], key: bindDarkMode[1] };
|
||||
}
|
||||
if (bindDarkMode && bindDarkMode.state && bindDarkMode.key) {
|
||||
const { state, key } = bindDarkMode;
|
||||
if (state.__watch) {
|
||||
if (state && key) {
|
||||
state.__watch(key, updateTheme);
|
||||
updateTheme(state[key]);
|
||||
}
|
||||
} else if (darkMode !== void 0) {
|
||||
updateTheme(darkMode);
|
||||
},
|
||||
/**
|
||||
* 动态配置主题变量
|
||||
* @param {Object} config { primary: '#6366f1', ... }
|
||||
*/
|
||||
config: (config = {}) => {
|
||||
if (typeof document === "undefined") return;
|
||||
const root = document.documentElement;
|
||||
const colors = ["primary", "secondary", "success", "info", "warning", "danger", "light", "dark"];
|
||||
colors.forEach((name) => {
|
||||
const hex = config[name];
|
||||
if (hex) {
|
||||
root.style.setProperty(`--bs-${name}`, hex);
|
||||
const rgb = Bootstrap._hexToRgb(hex);
|
||||
if (rgb) root.style.setProperty(`--bs-${name}-rgb`, `${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
_hexToRgb: (hex) => {
|
||||
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null;
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
};
|
||||
if (typeof globalThis !== "undefined") {
|
||||
globalThis.bootstrap = bootstrap;
|
||||
globalThis.Bootstrap = Bootstrap;
|
||||
}
|
||||
exports.Bootstrap = Bootstrap;
|
||||
exports.default = Bootstrap;
|
||||
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
||||
return exports;
|
||||
}({});
|
||||
export {
|
||||
Bootstrap,
|
||||
Bootstrap as default
|
||||
};
|
||||
|
||||
4
dist/bootstrap.min.js
vendored
4
dist/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.package-lock.json
generated
vendored
4
node_modules/.package-lock.json
generated
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@apigo.cc/bootstrap",
|
||||
"version": "1.0.1",
|
||||
"name": "bootstrap",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
5188
node_modules/.vite/deps_temp_ee6d9627/bootstrap.js
generated
vendored
Normal file
5188
node_modules/.vite/deps_temp_ee6d9627/bootstrap.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
node_modules/.vite/deps_temp_ee6d9627/bootstrap.js.map
generated
vendored
Normal file
7
node_modules/.vite/deps_temp_ee6d9627/bootstrap.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
node_modules/.vite/deps_temp_ee6d9627/package.json
generated
vendored
Normal file
3
node_modules/.vite/deps_temp_ee6d9627/package.json
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@apigo.cc/bootstrap",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.4",
|
||||
"type": "module",
|
||||
"main": "dist/bootstrap.js",
|
||||
"module": "dist/bootstrap.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
||||
150
src/index.js
150
src/index.js
@ -1,131 +1,63 @@
|
||||
import * as bootstrap from 'bootstrap'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
import { Hash, LocalStorage, RefreshState } from '@apigo.cc/state'
|
||||
import { Hash, LocalStorage, RefreshState } from '@web/state'
|
||||
|
||||
const GlobalStates = { Hash, LocalStorage }
|
||||
|
||||
/**
|
||||
* @apigo.cc/bootstrap
|
||||
* @web/bootstrap
|
||||
* 自包含的 Bootstrap 5.3 集成引擎
|
||||
*/
|
||||
|
||||
// 导出增强功能对象
|
||||
export const Bootstrap = {
|
||||
config: (options = {}) => {
|
||||
const Bootstrap = {
|
||||
// 原始 Bootstrap 实例引用
|
||||
...bootstrap,
|
||||
/**
|
||||
* 绑定暗色模式
|
||||
* @param {Object} state NewState 对象 (如 LocalStorage)
|
||||
* @param {string} key 键名 (如 'darkMode')
|
||||
*/
|
||||
bindDarkMode: (state, key) => {
|
||||
if (typeof document === 'undefined') return
|
||||
const root = document.documentElement
|
||||
|
||||
// 1. 处理颜色主题 (Colors)
|
||||
const colors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'light', 'dark']
|
||||
let cssPatch = ''
|
||||
|
||||
colors.forEach(name => {
|
||||
const hex = options[name]
|
||||
if (hex) {
|
||||
root.style.setProperty(`--bs-${name}`, hex)
|
||||
const rgb = Bootstrap._hexToRgb(hex)
|
||||
if (rgb) {
|
||||
const rgbStr = `${rgb.r}, ${rgb.g}, ${rgb.b}`
|
||||
root.style.setProperty(`--bs-${name}-rgb`, rgbStr)
|
||||
|
||||
// 深度补丁:覆盖组件内部硬编码的变量和状态样式
|
||||
cssPatch += `
|
||||
.btn-${name} {
|
||||
--bs-btn-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-border-color: var(--bs-${name}) !important;
|
||||
--bs-btn-hover-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-hover-border-color: var(--bs-${name}) !important;
|
||||
--bs-btn-active-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-active-border-color: var(--bs-${name}) !important;
|
||||
--bs-btn-disabled-bg: var(--bs-${name}) !important;
|
||||
--bs-btn-disabled-border-color: var(--bs-${name}) !important;
|
||||
}
|
||||
.bg-${name} { background-color: var(--bs-${name}) !important; }
|
||||
.text-${name} { color: var(--bs-${name}) !important; }
|
||||
.border-${name} { border-color: var(--bs-${name}) !important; }
|
||||
.badge.bg-${name} { background-color: var(--bs-${name}) !important; }
|
||||
|
||||
/* 表单控件补丁 (Form Controls) */
|
||||
.form-check-input:checked {
|
||||
background-color: var(--bs-primary) !important;
|
||||
border-color: var(--bs-primary) !important;
|
||||
}
|
||||
.form-check-input:focus {
|
||||
border-color: var(--bs-primary) !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25) !important;
|
||||
}
|
||||
.form-switch .form-check-input:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e") !important;
|
||||
}
|
||||
.form-range::-webkit-slider-thumb { background: var(--bs-primary) !important; }
|
||||
.form-range::-moz-range-thumb { background: var(--bs-primary) !important; }
|
||||
.form-range::-webkit-slider-thumb:active { background-color: rgba(var(--bs-primary-rgb), 0.5) !important; }
|
||||
|
||||
/* 进度条与分页 (Progress & Pagination) */
|
||||
.progress-bar { background-color: var(--bs-primary) !important; }
|
||||
.page-link { color: var(--bs-primary); }
|
||||
.active > .page-link, .page-link.active {
|
||||
background-color: var(--bs-primary) !important;
|
||||
border-color: var(--bs-primary) !important;
|
||||
}
|
||||
|
||||
/* 列表组 (List Group) */
|
||||
.list-group-item.active {
|
||||
background-color: var(--bs-primary) !important;
|
||||
border-color: var(--bs-primary) !important;
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (cssPatch) {
|
||||
let styleEl = document.getElementById('bs-config-patch')
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style')
|
||||
styleEl.id = 'bs-config-patch'
|
||||
document.head.appendChild(styleEl)
|
||||
}
|
||||
styleEl.innerHTML = cssPatch
|
||||
}
|
||||
|
||||
// 2. 处理暗黑模式 (Dark Mode)
|
||||
let { bindDarkMode, darkMode } = options
|
||||
const htmlNode = document.documentElement
|
||||
const updateTheme = (val) => {
|
||||
root.setAttribute('data-bs-theme', val ? 'dark' : 'light')
|
||||
htmlNode.setAttribute('data-bs-theme', val ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
// 支持 [state, key] 简写
|
||||
if (Array.isArray(bindDarkMode)) {
|
||||
bindDarkMode = { state: bindDarkMode[0], key: bindDarkMode[1] }
|
||||
}
|
||||
|
||||
if (bindDarkMode && bindDarkMode.state && bindDarkMode.key) {
|
||||
const { state, key } = bindDarkMode
|
||||
if (state.__watch) {
|
||||
if (state && key) {
|
||||
state.__watch(key, updateTheme)
|
||||
updateTheme(state[key])
|
||||
}
|
||||
} else if (darkMode !== undefined) {
|
||||
updateTheme(darkMode)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 动态配置主题变量
|
||||
* @param {Object} config { primary: '#6366f1', ... }
|
||||
*/
|
||||
config: (config = {}) => {
|
||||
if (typeof document === 'undefined') return
|
||||
const root = document.documentElement
|
||||
const colors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'light', 'dark']
|
||||
|
||||
colors.forEach(name => {
|
||||
const hex = config[name]
|
||||
if (hex) {
|
||||
root.style.setProperty(`--bs-${name}`, hex)
|
||||
const rgb = Bootstrap._hexToRgb(hex)
|
||||
if (rgb) root.style.setProperty(`--bs-${name}-rgb`, `${rgb.r}, ${rgb.g}, ${rgb.b}`)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
_hexToRgb: (hex) => {
|
||||
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b)
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null
|
||||
}
|
||||
}
|
||||
|
||||
// 挂载到全局
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
// 小写变量保持为纯原生或接近原生的引用,供 AI 舒适使用
|
||||
globalThis.bootstrap = bootstrap;
|
||||
// 大写变量供需要我们额外增强能力的场景使用
|
||||
globalThis.Bootstrap = Bootstrap;
|
||||
}
|
||||
|
||||
export default Bootstrap;
|
||||
|
||||
export { Bootstrap }
|
||||
export default Bootstrap
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@apigo.cc/state": "../state/src/index.js",
|
||||
"@apigo.cc/bootstrap": "./src/index.js"
|
||||
"@web/state": "../state/src/index.js",
|
||||
"@web/bootstrap": "./src/index.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -22,8 +22,8 @@
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { Bootstrap } from '@apigo.cc/bootstrap';
|
||||
import { LocalStorage } from '@apigo.cc/state';
|
||||
import { Bootstrap } from '@web/bootstrap';
|
||||
import { LocalStorage } from '@web/state';
|
||||
|
||||
// 初始化
|
||||
LocalStorage.darkMode = false;
|
||||
|
||||
@ -9,34 +9,32 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@apigo.cc/state': resolve(__dirname, '../state/src/index.js'),
|
||||
'@apigo.cc/bootstrap': resolve(__dirname, 'src/index.js')
|
||||
'@web/state': resolve(__dirname, '../state/src/index.js'),
|
||||
'@web/bootstrap': resolve(__dirname, 'src/index.js')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..']
|
||||
}
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
name: 'ApigoBootstrap',
|
||||
formats: ['iife']
|
||||
name: 'Bootstrap',
|
||||
formats: ['es']
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['@apigo.cc/state'],
|
||||
external: ['@web/state'],
|
||||
output: [
|
||||
{
|
||||
format: 'iife',
|
||||
name: 'ApigoBootstrap',
|
||||
format: 'es',
|
||||
entryFileNames: 'bootstrap.js',
|
||||
globals: {
|
||||
'@apigo.cc/state': 'ApigoState'
|
||||
}
|
||||
minifyInternalExports: false
|
||||
},
|
||||
{
|
||||
format: 'iife',
|
||||
name: 'ApigoBootstrap',
|
||||
format: 'es',
|
||||
entryFileNames: 'bootstrap.min.js',
|
||||
globals: {
|
||||
'@apigo.cc/state': 'ApigoState'
|
||||
},
|
||||
plugins: [terser()]
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user