@kupola/kupola 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/components-ext.css +157 -0
- package/css/components.css +69 -0
- package/css/theme-dark.css +5 -5
- package/css/utilities.css +119 -1
- package/dist/css/kupola.css +1 -12
- package/dist/css/kupola.min.css +1 -0
- package/dist/icons.svg +284 -0
- package/dist/kupola.cjs.js +17409 -315
- package/dist/kupola.cjs.js.map +1 -1
- package/dist/kupola.css +9000 -0
- package/dist/kupola.esm.js +17183 -8531
- package/dist/kupola.esm.js.map +1 -1
- package/dist/kupola.min.css +1 -0
- package/dist/kupola.min.js +2 -0
- package/dist/kupola.min.js.map +1 -0
- package/dist/kupola.umd.js +17415 -315
- package/dist/kupola.umd.js.map +1 -1
- package/js/initializer.js +38 -0
- package/js/theme-standalone.js +88 -0
- package/package.json +6 -2
- package/scripts/build-css.cjs +113 -0
- package/scripts/build-svg-sprite.cjs +107 -0
package/js/initializer.js
CHANGED
|
@@ -198,6 +198,44 @@ for (const comp of knownComponents) {
|
|
|
198
198
|
|
|
199
199
|
export { ComponentInitializerRegistry, kupolaInitializer };
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Kupola global namespace — convenience API for external consumers.
|
|
203
|
+
* Provides Kupola.refresh(root) to re-initialize dynamically inserted DOM.
|
|
204
|
+
*/
|
|
205
|
+
if (typeof window !== 'undefined') {
|
|
206
|
+
window.Kupola = window.Kupola || {};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Re-scan and initialize components within a given scope.
|
|
210
|
+
* @param {Element|Document} [root=document] - Scope to scan
|
|
211
|
+
* @returns {Promise<void>}
|
|
212
|
+
*/
|
|
213
|
+
window.Kupola.refresh = async function refresh(root) {
|
|
214
|
+
return kupolaInitializer.initializeAll(root || document);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Initialize a single element or container.
|
|
219
|
+
* @param {Element} element
|
|
220
|
+
* @returns {Promise<void>}
|
|
221
|
+
*/
|
|
222
|
+
window.Kupola.init = async function init(element) {
|
|
223
|
+
return kupolaInitializer.initialize(element);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Cleanup a single element.
|
|
228
|
+
* @param {Element} element
|
|
229
|
+
*/
|
|
230
|
+
window.Kupola.cleanup = function cleanup(element) {
|
|
231
|
+
return kupolaInitializer.cleanup(element);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** Expose the initializer registry directly */
|
|
235
|
+
window.Kupola.initializer = kupolaInitializer;
|
|
236
|
+
window.Kupola.ComponentInitializerRegistry = ComponentInitializerRegistry;
|
|
237
|
+
}
|
|
238
|
+
|
|
201
239
|
if (typeof window !== 'undefined') {
|
|
202
240
|
window.ComponentInitializerRegistry = ComponentInitializerRegistry;
|
|
203
241
|
window.kupolaInitializer = kupolaInitializer;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kupola Theme Standalone — lightweight theme toggle (~1KB).
|
|
3
|
+
*
|
|
4
|
+
* Usage: Include this single script in your page. No other Kupola JS required.
|
|
5
|
+
* <script src="theme-standalone.js"></script>
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Reads/writes theme preference from localStorage
|
|
9
|
+
* - Sets data-theme attribute on <html>
|
|
10
|
+
* - Auto-binds [data-theme-toggle] buttons
|
|
11
|
+
* - Supports prefers-color-scheme as default
|
|
12
|
+
* - Optional: auto-detects brand preference
|
|
13
|
+
*/
|
|
14
|
+
(function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
var THEME_KEY = 'kupola-theme';
|
|
18
|
+
var BRAND_KEY = 'kupola-brand';
|
|
19
|
+
|
|
20
|
+
function getPreferred() {
|
|
21
|
+
var saved = localStorage.getItem(THEME_KEY);
|
|
22
|
+
if (saved === 'dark' || saved === 'light') return saved;
|
|
23
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
24
|
+
return 'light';
|
|
25
|
+
}
|
|
26
|
+
return 'dark';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applyTheme(theme) {
|
|
30
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
31
|
+
localStorage.setItem(THEME_KEY, theme);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applyBrand(brand) {
|
|
35
|
+
if (brand) {
|
|
36
|
+
document.documentElement.setAttribute('data-brand', brand);
|
|
37
|
+
localStorage.setItem(BRAND_KEY, brand);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toggleTheme() {
|
|
42
|
+
var current = document.documentElement.getAttribute('data-theme') || getPreferred();
|
|
43
|
+
var next = current === 'dark' ? 'light' : 'dark';
|
|
44
|
+
applyTheme(next);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function bindToggleButtons() {
|
|
48
|
+
var buttons = document.querySelectorAll('[data-theme-toggle]');
|
|
49
|
+
buttons.forEach(function (btn) {
|
|
50
|
+
btn.addEventListener('click', function (e) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
toggleTheme();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Initialize immediately (works even before DOMContentLoaded)
|
|
58
|
+
applyTheme(getPreferred());
|
|
59
|
+
applyBrand(localStorage.getItem(BRAND_KEY));
|
|
60
|
+
|
|
61
|
+
// Bind toggle buttons when DOM is ready
|
|
62
|
+
if (document.readyState === 'loading') {
|
|
63
|
+
document.addEventListener('DOMContentLoaded', bindToggleButtons);
|
|
64
|
+
} else {
|
|
65
|
+
bindToggleButtons();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Listen for system theme changes
|
|
69
|
+
if (window.matchMedia) {
|
|
70
|
+
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
71
|
+
if (mq.addEventListener) {
|
|
72
|
+
mq.addEventListener('change', function (e) {
|
|
73
|
+
if (!localStorage.getItem(THEME_KEY)) {
|
|
74
|
+
applyTheme(e.matches ? 'dark' : 'light');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Expose minimal API
|
|
81
|
+
window.KupolaTheme = {
|
|
82
|
+
toggle: toggleTheme,
|
|
83
|
+
set: applyTheme,
|
|
84
|
+
get: function () {
|
|
85
|
+
return document.documentElement.getAttribute('data-theme') || 'dark';
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kupola/kupola",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A lightweight UI toolkit for any web project. No heavy frontend frameworks required.",
|
|
5
5
|
"main": "dist/kupola.cjs.js",
|
|
6
6
|
"module": "dist/kupola.esm.js",
|
|
@@ -57,6 +57,9 @@
|
|
|
57
57
|
"lint": "eslint .",
|
|
58
58
|
"lint:fix": "eslint . --fix",
|
|
59
59
|
"format": "prettier --write .",
|
|
60
|
+
"build:css": "node scripts/build-css.cjs",
|
|
61
|
+
"build:sprite": "node scripts/build-svg-sprite.cjs",
|
|
62
|
+
"build:all": "node scripts/build-css.cjs && node scripts/build-svg-sprite.cjs",
|
|
60
63
|
"test": "jest",
|
|
61
64
|
"test:watch": "jest --watch",
|
|
62
65
|
"test:coverage": "jest --coverage"
|
|
@@ -98,7 +101,8 @@
|
|
|
98
101
|
"icons/",
|
|
99
102
|
"plugins/",
|
|
100
103
|
"adapters/",
|
|
101
|
-
"types/"
|
|
104
|
+
"types/",
|
|
105
|
+
"scripts/"
|
|
102
106
|
],
|
|
103
107
|
"devDependencies": {
|
|
104
108
|
"@rollup/plugin-commonjs": "^25.0.7",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* build-css.js — Merge and minify Kupola CSS into a single file.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/build-css.cjs
|
|
7
|
+
*
|
|
8
|
+
* Output:
|
|
9
|
+
* dist/kupola.min.css — All CSS merged and minified
|
|
10
|
+
* dist/kupola.css — All CSS merged (unminified, for debugging)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const CSS_DIR = path.resolve(__dirname, '..', 'css');
|
|
17
|
+
const OUTPUT_DIR = path.resolve(__dirname, '..', 'dist');
|
|
18
|
+
|
|
19
|
+
// Import order must match kupola.css @import order
|
|
20
|
+
const IMPORT_ORDER = [
|
|
21
|
+
'colors_and_type.css',
|
|
22
|
+
'theme-light.css',
|
|
23
|
+
'theme-dark.css',
|
|
24
|
+
'brand-themes.css',
|
|
25
|
+
'scaffold.css',
|
|
26
|
+
'components.css',
|
|
27
|
+
'components-ext.css',
|
|
28
|
+
'states.css',
|
|
29
|
+
'utilities.css',
|
|
30
|
+
'responsive.css',
|
|
31
|
+
'accessibility.css',
|
|
32
|
+
'animations.css'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function resolveImports(cssContent, basePath) {
|
|
36
|
+
// Replace @import url('...') with actual file content
|
|
37
|
+
return cssContent.replace(/@import\s+url\(['"]?([^'")\s]+)['"]?\)\s*;/g, (match, url) => {
|
|
38
|
+
const filePath = path.join(basePath, url);
|
|
39
|
+
if (fs.existsSync(filePath)) {
|
|
40
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
// Recursively resolve imports
|
|
42
|
+
return resolveImports(content, path.dirname(filePath));
|
|
43
|
+
}
|
|
44
|
+
console.warn(` Warning: Import not found: ${url}`);
|
|
45
|
+
return `/* Import not found: ${url} */`;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function minifyCSS(css) {
|
|
50
|
+
return css
|
|
51
|
+
// Remove comments
|
|
52
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
53
|
+
// Remove extra whitespace
|
|
54
|
+
.replace(/\s+/g, ' ')
|
|
55
|
+
// Remove spaces around selectors/braces
|
|
56
|
+
.replace(/\s*{\s*/g, '{')
|
|
57
|
+
.replace(/\s*}\s*/g, '}')
|
|
58
|
+
.replace(/\s*;\s*/g, ';')
|
|
59
|
+
.replace(/\s*:\s*/g, ':')
|
|
60
|
+
.replace(/\s*,\s*/g, ',')
|
|
61
|
+
// Remove trailing semicolons before closing braces
|
|
62
|
+
.replace(/;}/g, '}')
|
|
63
|
+
// Remove leading/trailing whitespace
|
|
64
|
+
.trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildCSS() {
|
|
68
|
+
console.log('Building Kupola CSS...');
|
|
69
|
+
|
|
70
|
+
// Read main entry file
|
|
71
|
+
const mainFile = path.join(CSS_DIR, 'kupola.css');
|
|
72
|
+
if (!fs.existsSync(mainFile)) {
|
|
73
|
+
console.error(`Main CSS file not found: ${mainFile}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mainContent = fs.readFileSync(mainFile, 'utf-8');
|
|
78
|
+
|
|
79
|
+
// Resolve all @import statements
|
|
80
|
+
let merged = resolveImports(mainContent, CSS_DIR);
|
|
81
|
+
|
|
82
|
+
// Also check for any files not in the import list (like dashboard.css)
|
|
83
|
+
// and add them as optional separate output
|
|
84
|
+
|
|
85
|
+
// Ensure output directory exists
|
|
86
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
87
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Write unminified version
|
|
91
|
+
const unminPath = path.join(OUTPUT_DIR, 'kupola.css');
|
|
92
|
+
fs.writeFileSync(unminPath, merged, 'utf-8');
|
|
93
|
+
const unminSize = (fs.statSync(unminPath).size / 1024).toFixed(1);
|
|
94
|
+
console.log(` Unminified: ${unminPath} (${unminSize} KB)`);
|
|
95
|
+
|
|
96
|
+
// Write minified version
|
|
97
|
+
const minified = minifyCSS(merged);
|
|
98
|
+
const minPath = path.join(OUTPUT_DIR, 'kupola.min.css');
|
|
99
|
+
fs.writeFileSync(minPath, minified, 'utf-8');
|
|
100
|
+
const minSize = (fs.statSync(minPath).size / 1024).toFixed(1);
|
|
101
|
+
console.log(` Minified: ${minPath} (${minSize} KB)`);
|
|
102
|
+
|
|
103
|
+
// Estimate gzip size (rough approximation)
|
|
104
|
+
const gzipEstimate = (minSize * 0.25).toFixed(1);
|
|
105
|
+
console.log(` Est. gzip: ~${gzipEstimate} KB`);
|
|
106
|
+
|
|
107
|
+
// Summary
|
|
108
|
+
const files = IMPORT_ORDER.filter(f => fs.existsSync(path.join(CSS_DIR, f)));
|
|
109
|
+
console.log(` Source files: ${files.length}`);
|
|
110
|
+
console.log('CSS build complete!');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
buildCSS();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* build-svg-sprite.js — Merge individual SVG icons into a single SVG sprite.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/build-svg-sprite.js
|
|
7
|
+
*
|
|
8
|
+
* Output:
|
|
9
|
+
* dist/icons.svg — SVG sprite with <symbol> elements
|
|
10
|
+
*
|
|
11
|
+
* Usage in HTML:
|
|
12
|
+
* <svg width="16" height="16"><use href="icons.svg#icon-user"></use></svg>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const ICONS_DIR = path.resolve(__dirname, '..', 'icons');
|
|
19
|
+
const OUTPUT_DIR = path.resolve(__dirname, '..', 'dist');
|
|
20
|
+
const OUTPUT_FILE = path.join(OUTPUT_DIR, 'icons.svg');
|
|
21
|
+
|
|
22
|
+
// Skip files that are clearly not individual icons (sprites, variants with hashes)
|
|
23
|
+
const SKIP_PATTERNS = [/\.d\./, /\.[0-9a-f]{6}\./];
|
|
24
|
+
|
|
25
|
+
function shouldSkip(filename) {
|
|
26
|
+
return SKIP_PATTERNS.some(p => p.test(filename));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractSvgContent(svgString) {
|
|
30
|
+
// Extract viewBox from root <svg> element
|
|
31
|
+
const viewBoxMatch = svgString.match(/viewBox="([^"]+)"/);
|
|
32
|
+
const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
|
|
33
|
+
|
|
34
|
+
// Extract inner content (everything between <svg> and </svg>)
|
|
35
|
+
const innerMatch = svgString.match(/<svg[^>]*>([\s\S]*?)<\/svg>/i);
|
|
36
|
+
const inner = innerMatch ? innerMatch[1].trim() : '';
|
|
37
|
+
|
|
38
|
+
return { viewBox, inner };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildSprite() {
|
|
42
|
+
if (!fs.existsSync(ICONS_DIR)) {
|
|
43
|
+
console.error(`Icons directory not found: ${ICONS_DIR}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const files = fs.readdirSync(ICONS_DIR).filter(f => {
|
|
48
|
+
return f.endsWith('.svg') && !shouldSkip(f);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Deduplicate: if both "user.svg" and "user.0c0c0d.svg" exist, prefer "user.svg"
|
|
52
|
+
const seen = new Map();
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const baseName = file.replace(/\.[0-9a-f]{6}\.svg$/, '.svg');
|
|
55
|
+
const cleanName = baseName.replace('.svg', '');
|
|
56
|
+
|
|
57
|
+
if (!seen.has(cleanName) || !file.match(/\.[0-9a-f]{6}\.svg$/)) {
|
|
58
|
+
seen.set(cleanName, file);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const symbols = [];
|
|
63
|
+
let processed = 0;
|
|
64
|
+
|
|
65
|
+
for (const [name, file] of seen) {
|
|
66
|
+
const filePath = path.join(ICONS_DIR, file);
|
|
67
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const { viewBox, inner } = extractSvgContent(content);
|
|
71
|
+
if (!inner) continue;
|
|
72
|
+
|
|
73
|
+
const id = `icon-${name}`;
|
|
74
|
+
symbols.push(
|
|
75
|
+
` <symbol id="${id}" viewBox="${viewBox}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</symbol>`
|
|
76
|
+
);
|
|
77
|
+
processed++;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.warn(` Warning: Could not parse ${file}: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Sort symbols alphabetically for consistency
|
|
84
|
+
symbols.sort((a, b) => a.localeCompare(b));
|
|
85
|
+
|
|
86
|
+
const sprite = [
|
|
87
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
88
|
+
'<svg xmlns="http://www.w3.org/2000/svg" style="display:none">',
|
|
89
|
+
...symbols,
|
|
90
|
+
'</svg>',
|
|
91
|
+
''
|
|
92
|
+
].join('\n');
|
|
93
|
+
|
|
94
|
+
// Ensure output directory exists
|
|
95
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
96
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(OUTPUT_FILE, sprite, 'utf-8');
|
|
100
|
+
|
|
101
|
+
const sizeKB = (fs.statSync(OUTPUT_FILE).size / 1024).toFixed(1);
|
|
102
|
+
console.log(`SVG Sprite built: ${OUTPUT_FILE}`);
|
|
103
|
+
console.log(` Icons: ${processed}`);
|
|
104
|
+
console.log(` Size: ${sizeKB} KB`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
buildSprite();
|