@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/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.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();