vanillaforge 1.9.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.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * VanillaForge built-in icons plugin.
3
+ *
4
+ * Provides zero-dependency, inline-SVG icons so apps don't need Font Awesome
5
+ * or any other external icon library. Icons render as accessible <svg> elements
6
+ * directly in the HTML string — no external requests, no flash of missing icons.
7
+ *
8
+ * Usage:
9
+ * import { createApp, iconsPlugin } from './src/framework.js';
10
+ * const app = createApp({ ... });
11
+ * app.use(iconsPlugin); // install with defaults
12
+ * app.use(iconsPlugin, {
13
+ * icons: { logo: '<path .../>' } // add/override individual icons
14
+ * });
15
+ *
16
+ * In any component:
17
+ * getTemplate() {
18
+ * return `<button>${this.icon('check', { size: 18 })} Save</button>`;
19
+ * }
20
+ *
21
+ * Users can still bring their own icon library — just don't install this plugin
22
+ * and wire up whatever they prefer.
23
+ */
24
+
25
+ import { defaultIcons } from './default-icons.js';
26
+
27
+ /**
28
+ * Service that stores and renders inline SVG icons.
29
+ * Registered under the key 'icons' via app.provide().
30
+ */
31
+ export class IconsService {
32
+ constructor(icons = {}) {
33
+ this._icons = new Map(Object.entries(icons));
34
+ this._warnedUnknown = new Set();
35
+ }
36
+
37
+ /**
38
+ * Register a custom icon (or override an existing one).
39
+ *
40
+ * @param {string} name - Icon name
41
+ * @param {string} innerSvg - SVG inner content (everything inside <svg>...</svg>)
42
+ * @returns {IconsService} this, for chaining
43
+ */
44
+ register(name, innerSvg) {
45
+ this._icons.set(name, innerSvg);
46
+ return this;
47
+ }
48
+
49
+ /**
50
+ * Check whether a named icon is registered.
51
+ *
52
+ * @param {string} name
53
+ * @returns {boolean}
54
+ */
55
+ has(name) {
56
+ return this._icons.has(name);
57
+ }
58
+
59
+ /**
60
+ * Render a named icon as an inline SVG string.
61
+ *
62
+ * @param {string} name - Icon name (e.g. 'check', 'trash', 'menu')
63
+ * @param {Object} [opts={}]
64
+ * @param {number} [opts.size=24] - Width and height in pixels
65
+ * @param {string} [opts.className=''] - Extra CSS class on the <svg> element
66
+ * @param {string} [opts.title] - Accessible title; omit for decorative icons
67
+ * @param {string} [opts.color] - Inline color override (defaults to currentColor)
68
+ * @returns {string} Inline SVG string, or '' for unknown icons
69
+ */
70
+ render(name, opts = {}) {
71
+ const inner = this._icons.get(name);
72
+ if (!inner) {
73
+ if (!this._warnedUnknown.has(name)) {
74
+ console.warn(`[VanillaForge icons] Unknown icon: "${name}"`);
75
+ this._warnedUnknown.add(name);
76
+ }
77
+ return '';
78
+ }
79
+
80
+ const size = opts.size ?? 24;
81
+ const cls = ['vf-icon', opts.className].filter(Boolean).join(' ');
82
+ const colorStyle = opts.color ? ` style="color:${opts.color}"` : '';
83
+ const titleTag = opts.title
84
+ ? `<title>${escapeHtml(opts.title)}</title>`
85
+ : '';
86
+ const aria = opts.title
87
+ ? `role="img" aria-label="${escapeHtml(opts.title)}"`
88
+ : 'aria-hidden="true"';
89
+
90
+ return (
91
+ `<svg xmlns="http://www.w3.org/2000/svg" ` +
92
+ `class="${cls}" ` +
93
+ `width="${size}" height="${size}" ` +
94
+ `viewBox="0 0 24 24" ` +
95
+ `fill="none" ` +
96
+ `${aria}` +
97
+ `${colorStyle}>` +
98
+ `${titleTag}${inner}` +
99
+ `</svg>`
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Plugin object. Install with app.use(iconsPlugin) or
106
+ * app.use(iconsPlugin, { icons: { myIcon: '<path .../>' } }).
107
+ *
108
+ * Options:
109
+ * icons {Object} - Additional icon definitions merged on top of the defaults.
110
+ * A key that already exists in defaults is overridden.
111
+ */
112
+ export const iconsPlugin = {
113
+ name: 'icons',
114
+
115
+ install(app, options = {}) {
116
+ const merged = { ...defaultIcons, ...(options.icons || {}) };
117
+ const service = new IconsService(merged);
118
+ app.provide('icons', service);
119
+ },
120
+ };
121
+
122
+ // ---- private helpers ----
123
+
124
+ function escapeHtml(str) {
125
+ return String(str)
126
+ .replace(/&/g, '&amp;')
127
+ .replace(/"/g, '&quot;')
128
+ .replace(/</g, '&lt;')
129
+ .replace(/>/g, '&gt;');
130
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Store plugin — shared reactive state for VanillaForge applications.
3
+ *
4
+ * Provides a lightweight key/value store layered on top of the EventBus.
5
+ * Any component (or plugin) can read, write, and subscribe to named keys.
6
+ *
7
+ * Usage:
8
+ *
9
+ * app.use(storePlugin);
10
+ *
11
+ * // From a component:
12
+ * const store = this.service('store');
13
+ *
14
+ * store.set('cart', [...items]); // write
15
+ * store.get('cart'); // read (current value)
16
+ * const unsub = store.subscribe('cart', (items) => {
17
+ * this.setState({ items });
18
+ * });
19
+ * unsub(); // stop listening
20
+ *
21
+ * Events emitted on the shared EventBus:
22
+ * 'store:change' — { key, value, prev } — every time a key changes.
23
+ * 'store:change:<key>' — { value, prev } — per-key variant for targeted listeners.
24
+ */
25
+
26
+ export class StoreService {
27
+ /**
28
+ * @param {import('../../core/event-bus.js').EventBus} eventBus
29
+ */
30
+ constructor(eventBus) {
31
+ this._bus = eventBus;
32
+ this._state = new Map();
33
+ }
34
+
35
+ /**
36
+ * Write a value to the store under `key`.
37
+ * Identical values (via Object.is) are silently ignored — no events fired.
38
+ *
39
+ * @param {string} key
40
+ * @param {*} value
41
+ */
42
+ set(key, value) {
43
+ const prev = this._state.get(key);
44
+ if (Object.is(prev, value)) return;
45
+ this._state.set(key, value);
46
+ this._bus.emit('store:change', { key, value, prev });
47
+ this._bus.emit(`store:change:${key}`, { value, prev });
48
+ }
49
+
50
+ /**
51
+ * Read the current value for `key`.
52
+ * Returns `undefined` when the key has never been written.
53
+ *
54
+ * @param {string} key
55
+ * @returns {*}
56
+ */
57
+ get(key) {
58
+ return this._state.get(key);
59
+ }
60
+
61
+ /**
62
+ * Subscribe to changes for a specific `key`.
63
+ * The handler is called with `(value, prev)` whenever the key changes.
64
+ * Returns an unsubscribe function.
65
+ *
66
+ * @param {string} key
67
+ * @param {(value: *, prev: *) => void} handler
68
+ * @returns {() => void} unsubscribe
69
+ */
70
+ subscribe(key, handler) {
71
+ return this._bus.on(`store:change:${key}`, ({ value, prev }) => {
72
+ handler(value, prev);
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Subscribe to ALL store changes.
78
+ * The handler is called with `(key, value, prev)` on every write.
79
+ * Returns an unsubscribe function.
80
+ *
81
+ * @param {(key: string, value: *, prev: *) => void} handler
82
+ * @returns {() => void} unsubscribe
83
+ */
84
+ subscribeAll(handler) {
85
+ return this._bus.on('store:change', ({ key, value, prev }) => {
86
+ handler(key, value, prev);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Remove a key from the store and fire change events with `value: undefined`.
92
+ *
93
+ * @param {string} key
94
+ */
95
+ delete(key) {
96
+ if (!this._state.has(key)) return;
97
+ const prev = this._state.get(key);
98
+ this._state.delete(key);
99
+ this._bus.emit('store:change', { key, value: undefined, prev });
100
+ this._bus.emit(`store:change:${key}`, { value: undefined, prev });
101
+ }
102
+
103
+ /**
104
+ * Returns all keys currently in the store.
105
+ * @returns {string[]}
106
+ */
107
+ keys() {
108
+ return Array.from(this._state.keys());
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Install the store plugin.
114
+ *
115
+ * Registers a `StoreService` instance as the 'store' service.
116
+ * After install, components access it via `this.service('store')`.
117
+ *
118
+ * @type {import('../../framework.js').Plugin}
119
+ */
120
+ export const storePlugin = {
121
+ name: 'store',
122
+
123
+ install(app) {
124
+ const store = new StoreService(app.eventBus);
125
+ app.provide('store', store);
126
+ },
127
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * VanillaForge base stylesheet.
3
+ *
4
+ * Injected by ThemeService when base !== false (the default).
5
+ * Every rule uses the --vf-* custom properties set by the token block,
6
+ * so swapping tokens is enough to retheme everything here.
7
+ */
8
+
9
+ export const BASE_STYLES = `
10
+ /* --- VanillaForge base --- */
11
+ *, *::before, *::after { box-sizing: border-box; }
12
+
13
+ body {
14
+ font-family: var(--vf-font-sans);
15
+ color: var(--vf-text);
16
+ background: var(--vf-background);
17
+ line-height: 1.5;
18
+ margin: 0;
19
+ }
20
+
21
+ a { color: var(--vf-primary); }
22
+ a:hover { color: var(--vf-primary-dark); }
23
+
24
+ /* --- .vf-card --- */
25
+ .vf-card {
26
+ background: var(--vf-surface);
27
+ border-radius: var(--vf-radius-lg);
28
+ box-shadow: var(--vf-shadow-md);
29
+ padding: 1.5rem;
30
+ }
31
+
32
+ /* --- .vf-btn --- */
33
+ .vf-btn {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: 6px;
37
+ padding: 8px 16px;
38
+ border-radius: var(--vf-radius);
39
+ border: none;
40
+ cursor: pointer;
41
+ font: inherit;
42
+ font-weight: 600;
43
+ font-size: .9rem;
44
+ text-decoration: none;
45
+ transition: opacity .15s;
46
+ }
47
+ .vf-btn:hover { opacity: .85; }
48
+ .vf-btn:active { opacity: .7; }
49
+ .vf-btn:disabled { opacity: .45; cursor: not-allowed; }
50
+
51
+ .vf-btn-primary { background: var(--vf-primary); color: #fff; }
52
+ .vf-btn-secondary { background: var(--vf-border); color: var(--vf-text); }
53
+ .vf-btn-danger { background: var(--vf-danger); color: #fff; }
54
+ .vf-btn-success { background: var(--vf-success); color: #fff; }
55
+
56
+ /* --- .vf-icon (icons plugin companion) --- */
57
+ .vf-icon { vertical-align: middle; }
58
+ `;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * VanillaForge built-in theme plugin.
3
+ *
4
+ * Injects a block of CSS custom properties (--vf-*) into <head> and,
5
+ * by default, a small base stylesheet that makes apps look sensible
6
+ * out of the box without Tailwind, Bootstrap, or any external CSS.
7
+ *
8
+ * Usage:
9
+ * import { createApp, themePlugin } from './src/framework.js';
10
+ * const app = createApp({ ... });
11
+ *
12
+ * app.use(themePlugin); // defaults only
13
+ * app.use(themePlugin, {
14
+ * tokens: { primary: '#6366f1', radius: '8px' }
15
+ * });
16
+ * app.use(themePlugin, { base: false }); // token vars only, no base stylesheet
17
+ *
18
+ * In any component:
19
+ * this.service('theme').setTokens({ primary: '#ef4444' }); // live update
20
+ * this.service('theme').getToken('primary'); // '#ef4444'
21
+ *
22
+ * In CSS / inline styles:
23
+ * color: var(--vf-primary);
24
+ * border-radius: var(--vf-radius);
25
+ */
26
+
27
+ import { BASE_STYLES } from './base-styles.js';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Default design tokens
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const DEFAULT_TOKENS = {
34
+ // Colors
35
+ primary: '#3b82f6',
36
+ primaryDark: '#2563eb',
37
+ secondary: '#6b7280',
38
+ surface: '#ffffff',
39
+ background: '#f4f5f7',
40
+ text: '#1f2933',
41
+ textMuted: '#7b8794',
42
+ border: '#e5e7eb',
43
+ danger: '#ef4444',
44
+ success: '#10b981',
45
+ warning: '#f59e0b',
46
+ // Shape
47
+ radius: '6px',
48
+ radiusSm: '4px',
49
+ radiusLg: '12px',
50
+ // Typography
51
+ fontSans: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
52
+ fontMono: '"JetBrains Mono", "Fira Code", monospace',
53
+ // Shadows
54
+ shadowSm: '0 1px 3px rgba(0,0,0,.08)',
55
+ shadowMd: '0 4px 16px rgba(0,0,0,.08)',
56
+ shadowLg: '0 10px 30px rgba(0,0,0,.12)',
57
+ // Base spacing unit (useful for calc()-based layouts)
58
+ space: '4px',
59
+ };
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // ThemeService
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Manages design tokens and injects them as CSS custom properties.
67
+ * Registered under the key 'theme' via app.provide().
68
+ */
69
+ export class ThemeService {
70
+ constructor(options = {}) {
71
+ this._tokens = { ...DEFAULT_TOKENS, ...(options.tokens || {}) };
72
+ this._includeBase = options.base !== false;
73
+ this._styleEl = null;
74
+ this._inject();
75
+ }
76
+
77
+ /**
78
+ * Update one or more tokens and re-inject the style block.
79
+ *
80
+ * @param {Object} tokens - Partial token map to merge in
81
+ * @returns {ThemeService} this, for chaining
82
+ */
83
+ setTokens(tokens) {
84
+ Object.assign(this._tokens, tokens);
85
+ this._inject();
86
+ return this;
87
+ }
88
+
89
+ /**
90
+ * Read a single token value by its camelCase name.
91
+ *
92
+ * @param {string} name - e.g. 'primary', 'fontSans'
93
+ * @returns {string|null}
94
+ */
95
+ getToken(name) {
96
+ return Object.prototype.hasOwnProperty.call(this._tokens, name)
97
+ ? this._tokens[name]
98
+ : null;
99
+ }
100
+
101
+ // ---- private -----------------------------------------------------------
102
+
103
+ _inject() {
104
+ if (typeof document === 'undefined') return;
105
+
106
+ if (!this._styleEl) {
107
+ // Reuse an element a previous ThemeService instance may have left behind
108
+ // (e.g. hot-reload or a second app.use() call after override).
109
+ this._styleEl = document.getElementById('vf-theme');
110
+ if (!this._styleEl) {
111
+ this._styleEl = document.createElement('style');
112
+ this._styleEl.id = 'vf-theme';
113
+ document.head.appendChild(this._styleEl);
114
+ }
115
+ }
116
+
117
+ this._styleEl.textContent = this._buildCSS();
118
+ }
119
+
120
+ _buildCSS() {
121
+ const vars = Object.entries(this._tokens)
122
+ .map(([k, v]) => ` --vf-${camelToKebab(k)}: ${v};`)
123
+ .join('\n');
124
+ const rootBlock = `:root {\n${vars}\n}`;
125
+ return this._includeBase
126
+ ? `${rootBlock}\n${BASE_STYLES}`
127
+ : rootBlock;
128
+ }
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Plugin export
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /**
136
+ * Plugin object. Install with:
137
+ * app.use(themePlugin)
138
+ * app.use(themePlugin, { tokens: { primary: '#6366f1' } })
139
+ * app.use(themePlugin, { base: false })
140
+ *
141
+ * Options:
142
+ * tokens {Object} - Token overrides merged on top of defaults.
143
+ * base {boolean} - Set to false to skip the base stylesheet (default: true).
144
+ */
145
+ export const themePlugin = {
146
+ name: 'theme',
147
+
148
+ install(app, options = {}) {
149
+ const service = new ThemeService(options);
150
+ app.provide('theme', service);
151
+ },
152
+ };
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Private helpers
156
+ // ---------------------------------------------------------------------------
157
+
158
+ function camelToKebab(str) {
159
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase();
160
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Decorators for performance and caching
3
+ */
4
+
5
+ import { performanceUtils } from './performance.js';
6
+
7
+ /**
8
+ * Performance decorator for methods
9
+ * @param {string} label - Performance label
10
+ * @returns {Function} Decorator function
11
+ */
12
+ export function perf(label) {
13
+ return function(target, propertyKey, descriptor) {
14
+ const originalMethod = descriptor.value;
15
+
16
+ descriptor.value = function(...args) {
17
+ return performanceUtils.measure(
18
+ () => originalMethod.apply(this, args),
19
+ `${target.constructor.name}.${propertyKey}${label ? ` (${label})` : ''}`
20
+ );
21
+ };
22
+
23
+ return descriptor;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Cache decorator for methods
29
+ * @param {number} ttl - Time to live in milliseconds
30
+ * @returns {Function} Decorator function
31
+ */
32
+ export function cache(ttl = 300000) {
33
+ return function(target, propertyKey, descriptor) {
34
+ const originalMethod = descriptor.value;
35
+
36
+ descriptor.value = function(...args) {
37
+ const cacheKey = `${target.constructor.name}.${propertyKey}.${JSON.stringify(args)}`;
38
+ const cached = performanceUtils.getCache(cacheKey);
39
+
40
+ if (cached !== null) {
41
+ return cached;
42
+ }
43
+
44
+ const result = originalMethod.apply(this, args);
45
+ performanceUtils.setCache(cacheKey, result, ttl);
46
+ return result;
47
+ };
48
+
49
+ return descriptor;
50
+ };
51
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * DOM-related utilities
3
+ */
4
+
5
+ /**
6
+ * Optimize images for different screen sizes
7
+ * @param {HTMLImageElement} img - Image element
8
+ * @param {Object} sizes - Size configuration
9
+ */
10
+ export function optimizeImage(img, sizes = {}) {
11
+ const defaultSizes = {
12
+ small: '(max-width: 480px)',
13
+ medium: '(max-width: 768px)',
14
+ large: '(min-width: 769px)'
15
+ };
16
+
17
+ const imageSizes = { ...defaultSizes, ...sizes };
18
+
19
+ const picture = document.createElement('picture');
20
+
21
+ Object.entries(imageSizes).forEach(([size, media]) => {
22
+ const source = document.createElement('source');
23
+ source.media = media;
24
+ source.srcset = img.dataset[`src${size.charAt(0).toUpperCase() + size.slice(1)}`] || img.src;
25
+ picture.appendChild(source);
26
+ });
27
+
28
+ picture.appendChild(img.cloneNode(true));
29
+ return picture;
30
+ }
31
+
32
+ /**
33
+ * Batch DOM operations to avoid layout thrashing
34
+ * @param {Function[]} operations - Array of DOM operations
35
+ */
36
+ export function batchDOMOperations(operations) {
37
+ requestAnimationFrame(() => {
38
+ operations.forEach(operation => operation());
39
+ });
40
+ }