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.
- package/CHANGELOG.md +466 -0
- package/README.md +198 -0
- package/package.json +91 -0
- package/src/components/base-component.js +925 -0
- package/src/core/component-manager.js +306 -0
- package/src/core/dom-morph.js +234 -0
- package/src/core/event-bus.js +229 -0
- package/src/core/router.js +487 -0
- package/src/core/signal.js +114 -0
- package/src/framework.js +323 -0
- package/src/plugins/alerts/alerts-plugin.js +427 -0
- package/src/plugins/fonts/files/inter.js +4 -0
- package/src/plugins/fonts/files/jetbrains-mono.js +4 -0
- package/src/plugins/fonts/font-manifests.js +53 -0
- package/src/plugins/fonts/fonts-plugin.js +246 -0
- package/src/plugins/icons/default-icons.js +51 -0
- package/src/plugins/icons/icons-plugin.js +130 -0
- package/src/plugins/store/store-plugin.js +127 -0
- package/src/plugins/theme/base-styles.js +58 -0
- package/src/plugins/theme/theme-plugin.js +160 -0
- package/src/utils/decorators.js +51 -0
- package/src/utils/dom.js +40 -0
- package/src/utils/error-handler.js +442 -0
- package/src/utils/framework-debug.js +375 -0
- package/src/utils/logger.js +324 -0
- package/src/utils/notification.js +123 -0
- package/src/utils/performance.js +281 -0
- package/src/utils/storage.js +86 -0
- package/src/utils/sweet-alert.js +84 -0
- package/src/utils/validation.js +70 -0
- package/src/utils/validators.js +129 -0
- package/types/index.d.ts +524 -0
|
@@ -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, '&')
|
|
127
|
+
.replace(/"/g, '"')
|
|
128
|
+
.replace(/</g, '<')
|
|
129
|
+
.replace(/>/g, '>');
|
|
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
|
+
}
|
package/src/utils/dom.js
ADDED
|
@@ -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
|
+
}
|