@okalit/cli 0.1.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.
Files changed (34) hide show
  1. package/README.md +32 -0
  2. package/bin/okalit-cli.js +8 -0
  3. package/lib/cli.js +515 -0
  4. package/package.json +30 -0
  5. package/templates/app/@okalit/AppMixin.js +29 -0
  6. package/templates/app/@okalit/EventBus.js +152 -0
  7. package/templates/app/@okalit/ModuleMixin.js +7 -0
  8. package/templates/app/@okalit/Okalit.js +129 -0
  9. package/templates/app/@okalit/OkalitService.js +145 -0
  10. package/templates/app/@okalit/defineElement.js +65 -0
  11. package/templates/app/@okalit/i18n.js +89 -0
  12. package/templates/app/@okalit/idle.js +40 -0
  13. package/templates/app/@okalit/index.js +10 -0
  14. package/templates/app/@okalit/lazy.js +32 -0
  15. package/templates/app/@okalit/okalit-router.js +309 -0
  16. package/templates/app/@okalit/service.js +33 -0
  17. package/templates/app/@okalit/trigger.js +14 -0
  18. package/templates/app/@okalit/viewport.js +69 -0
  19. package/templates/app/@okalit/when.js +40 -0
  20. package/templates/app/babel.config.json +5 -0
  21. package/templates/app/index.html +15 -0
  22. package/templates/app/package.json +23 -0
  23. package/templates/app/public/i18n/en.json +3 -0
  24. package/templates/app/public/i18n/es.json +3 -0
  25. package/templates/app/public/lit.svg +1 -0
  26. package/templates/app/src/app.routes.ts +10 -0
  27. package/templates/app/src/main-app.js +13 -0
  28. package/templates/app/src/modules/example/example.module.js +4 -0
  29. package/templates/app/src/modules/example/example.routes.js +7 -0
  30. package/templates/app/src/modules/example/pages/example.page.js +43 -0
  31. package/templates/app/src/modules/example/pages/example.page.scss +76 -0
  32. package/templates/app/src/styles/global.scss +0 -0
  33. package/templates/app/src/styles/index.css +4 -0
  34. package/templates/app/vite.config.js +19 -0
@@ -0,0 +1,152 @@
1
+ // Prevents crashes from tampered or corrupted storage values
2
+ function safeJsonParse(str, fallback = undefined) {
3
+ try {
4
+ return JSON.parse(str);
5
+ } catch {
6
+ return fallback;
7
+ }
8
+ }
9
+
10
+ class EventBusImpl {
11
+ constructor() {
12
+ this.listeners = Object.create(null);
13
+ this.triggers = Object.create(null);
14
+ this.persistTypes = {
15
+ memory: null,
16
+ session: window.sessionStorage,
17
+ local: window.localStorage
18
+ };
19
+ this.memoryStore = Object.create(null);
20
+ }
21
+
22
+ _readPersistedValue(event, persist = 'memory') {
23
+ if (persist === 'memory') {
24
+ const found = event in this.memoryStore;
25
+ return {
26
+ found,
27
+ value: found ? this.memoryStore[event] : undefined,
28
+ };
29
+ }
30
+
31
+ const storage = this.persistTypes[persist];
32
+ if (!storage) {
33
+ return {
34
+ found: false,
35
+ value: undefined,
36
+ };
37
+ }
38
+
39
+ const stored = storage.getItem(`okalit:bus:${event}`);
40
+ return {
41
+ found: stored !== null,
42
+ value: stored !== null ? safeJsonParse(stored) : undefined,
43
+ };
44
+ }
45
+
46
+ // Validate event name to prevent accidental misuse
47
+ _validateEvent(event) {
48
+ if (typeof event !== 'string' || event.trim() === '') {
49
+ throw new Error(`EventBus: invalid event name "${event}"`);
50
+ }
51
+ }
52
+
53
+ // Remove a specific channel from memory, session or local storage
54
+ remove(event, { persist = 'memory' } = {}) {
55
+ this._validateEvent(event);
56
+ if (persist === 'memory') {
57
+ delete this.memoryStore[event];
58
+ } else {
59
+ const storage = this.persistTypes[persist];
60
+ if (storage) {
61
+ storage.removeItem(`okalit:bus:${event}`);
62
+ }
63
+ }
64
+ }
65
+
66
+ // Clear all channels of a given persistence type
67
+ clearAll({ persist = 'memory' } = {}) {
68
+ if (persist === 'memory') {
69
+ this.memoryStore = Object.create(null);
70
+ } else {
71
+ const storage = this.persistTypes[persist];
72
+ if (storage) {
73
+ // Collect keys first to avoid mutating storage during iteration
74
+ const keys = [];
75
+ for (let i = 0; i < storage.length; i++) {
76
+ const k = storage.key(i);
77
+ if (k && k.startsWith('okalit:bus:')) keys.push(k);
78
+ }
79
+ keys.forEach(k => storage.removeItem(k));
80
+ }
81
+ }
82
+ }
83
+
84
+ // Emit a stateful event (persists value and notifies subscribers)
85
+ emit(event, data, { persist = 'memory' } = {}) {
86
+ this._validateEvent(event);
87
+ if (persist !== 'memory') {
88
+ const storage = this.persistTypes[persist];
89
+ if (storage) {
90
+ storage.setItem(`okalit:bus:${event}`, JSON.stringify(data));
91
+ }
92
+ } else {
93
+ this.memoryStore[event] = data;
94
+ }
95
+ // Notify all subscribers
96
+ const cbs = this.listeners[event];
97
+ if (cbs) {
98
+ for (let i = 0; i < cbs.length; i++) cbs[i](data);
99
+ }
100
+ }
101
+
102
+ get(event, { persist = 'memory', fallback } = {}) {
103
+ this._validateEvent(event);
104
+ const { found, value } = this._readPersistedValue(event, persist);
105
+
106
+ if (!found || value === undefined) {
107
+ return fallback;
108
+ }
109
+
110
+ return value;
111
+ }
112
+
113
+ // Trigger a stateless (ephemeral) event — no persistence
114
+ trigger(event, data) {
115
+ this._validateEvent(event);
116
+ const cbs = this.triggers[event];
117
+ if (cbs) {
118
+ for (let i = 0; i < cbs.length; i++) cbs[i](data);
119
+ }
120
+ }
121
+
122
+ // Subscribe to a stateful channel; delivers persisted value immediately if available
123
+ on(event, cb, { persist = 'memory', immediate = true } = {}) {
124
+ this._validateEvent(event);
125
+ if (!this.listeners[event]) this.listeners[event] = [];
126
+ this.listeners[event].push(cb);
127
+
128
+ // Deliver persisted value immediately on subscribe
129
+ if (immediate) {
130
+ const { found, value } = this._readPersistedValue(event, persist);
131
+ if (found && value !== undefined) cb(value);
132
+ }
133
+
134
+ // Return unsubscribe function
135
+ return () => {
136
+ this.listeners[event] = (this.listeners[event] || []).filter(fn => fn !== cb);
137
+ };
138
+ }
139
+
140
+ // Subscribe to ephemeral (stateless) events
141
+ listen(event, cb) {
142
+ this._validateEvent(event);
143
+ if (!this.triggers[event]) this.triggers[event] = [];
144
+ this.triggers[event].push(cb);
145
+ // Return unsubscribe function
146
+ return () => {
147
+ this.triggers[event] = (this.triggers[event] || []).filter(fn => fn !== cb);
148
+ };
149
+ }
150
+ }
151
+
152
+ export const EventBus = new EventBusImpl();
@@ -0,0 +1,7 @@
1
+ import { html } from "lit";
2
+
3
+ export const ModuleMixin = (Base) => class extends Base {
4
+ render() {
5
+ return html`<slot></slot>`;
6
+ }
7
+ }
@@ -0,0 +1,129 @@
1
+
2
+ import { LitElement } from 'lit';
3
+ import { EventBus } from './EventBus.js';
4
+ import { OkalitI18n } from './i18n.js';
5
+ import {signal, SignalWatcher} from '@lit-labs/signals';
6
+
7
+ export class Okalit extends SignalWatcher(LitElement) {
8
+ constructor() {
9
+ super();
10
+
11
+ this._okalit_channel_unsubs = [];
12
+ this._okalit_listen_unsubs = [];
13
+
14
+ const props = this.constructor.properties || {};
15
+ for (const [key, opts] of Object.entries(props)) {
16
+ if ('value' in opts && this[key] === undefined) {
17
+ this[key] = opts.value;
18
+ }
19
+ }
20
+ }
21
+
22
+ connectedCallback() {
23
+ super.connectedCallback();
24
+
25
+ if (typeof this.okalitConnections === 'function') {
26
+ this.okalitConnections();
27
+ }
28
+ }
29
+
30
+ disconnectedCallback() {
31
+ super.disconnectedCallback();
32
+
33
+ if (this._okalit_channel_unsubs.length > 0) {
34
+ this._okalit_channel_unsubs.forEach(unsub => unsub());
35
+ this._okalit_channel_unsubs = [];
36
+ }
37
+
38
+ if (this._okalit_listen_unsubs.length > 0) {
39
+ this._okalit_listen_unsubs.forEach(unsub => unsub());
40
+ this._okalit_listen_unsubs = [];
41
+ }
42
+
43
+ // Clean up @trigger decorator subscriptions
44
+ if (this._okalit_trigger_unsubs && this._okalit_trigger_unsubs.length > 0) {
45
+ this._okalit_trigger_unsubs.forEach(unsub => unsub());
46
+ this._okalit_trigger_unsubs = [];
47
+ }
48
+ }
49
+
50
+ output(eventName, detail) {
51
+ this.dispatchEvent(new CustomEvent(eventName, {
52
+ detail,
53
+ bubbles: true,
54
+ composed: true
55
+ }));
56
+ }
57
+
58
+ emit(event, data, options) {
59
+ EventBus.emit(event, data, options);
60
+ }
61
+
62
+ trigger(event, data) {
63
+ EventBus.trigger(event, data);
64
+ }
65
+
66
+ navigate(path, args) {
67
+ EventBus.trigger('okalit-route:navigate', { path, args });
68
+ }
69
+
70
+ // Called by the router to inject route + query params.
71
+ // Coerces values to the types declared in defineElement({ params }).
72
+ set routeParams(obj) {
73
+ if (!obj || typeof obj !== 'object') return;
74
+ const schema = this.constructor.__okalitParams;
75
+ for (const [key, raw] of Object.entries(obj)) {
76
+ if (schema && schema[key]) {
77
+ const typeCtor = schema[key].type;
78
+ if (typeCtor === Number) this[key] = Number(raw);
79
+ else if (typeCtor === Boolean) this[key] = raw === 'true' || raw === '1' || raw === true;
80
+ else this[key] = String(raw);
81
+ } else {
82
+ // No schema — assign as-is (string from URL)
83
+ this[key] = raw;
84
+ }
85
+ }
86
+ }
87
+
88
+ listen(event, callback) {
89
+ const unsub = EventBus.listen(event, callback.bind(this));
90
+ this._okalit_listen_unsubs.push(unsub);
91
+ }
92
+
93
+ t(key, params) {
94
+ return OkalitI18n.t(key, params);
95
+ }
96
+
97
+ channel(event, { persist = 'memory', initialValue, onValue } = {}) {
98
+ const currentValue = EventBus.get(event, { persist, fallback: initialValue });
99
+ const s = signal(currentValue);
100
+
101
+ const handleValue = (val) => {
102
+ s.set(val);
103
+ if (typeof onValue === 'function') {
104
+ onValue.call(this, val);
105
+ }
106
+ this.requestUpdate();
107
+ };
108
+
109
+ // Subscribe to channel changes
110
+ const unsub = EventBus.on(event, handleValue, {
111
+ persist,
112
+ immediate: false,
113
+ });
114
+
115
+ this._okalit_channel_unsubs.push(unsub);
116
+
117
+ if (typeof onValue === 'function') {
118
+ onValue.call(this, currentValue);
119
+ }
120
+
121
+ return {
122
+ get: () => s.get(),
123
+ set: (v) => {
124
+ s.set(v);
125
+ EventBus.emit(event, v, { persist });
126
+ }
127
+ };
128
+ }
129
+ }
@@ -0,0 +1,145 @@
1
+ import { EventBus } from './EventBus.js';
2
+
3
+ export class OkalitService {
4
+ constructor() {
5
+ this.baseUrl = '';
6
+ this.headers = {};
7
+ this.__cache = new Map();
8
+ this.__debounceTimers = new Map();
9
+ this.__activeRequests = new Map();
10
+ }
11
+
12
+ _makeCacheKey(path, params) {
13
+ let key = path;
14
+ if (params && typeof params === 'object') {
15
+ key += '?' + Object.entries(params).sort().map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
16
+ }
17
+ return key;
18
+ }
19
+
20
+ _debounce(cacheKey, delay, fn) {
21
+ if (this.__debounceTimers.has(cacheKey)) {
22
+ clearTimeout(this.__debounceTimers.get(cacheKey));
23
+ }
24
+ return new Promise((resolve, reject) => {
25
+ this.__debounceTimers.set(cacheKey, setTimeout(async () => {
26
+ this.__debounceTimers.delete(cacheKey);
27
+ try { resolve(await fn()); }
28
+ catch (err) { reject(err); }
29
+ }, delay));
30
+ });
31
+ }
32
+
33
+ async _parseJson(res, path) {
34
+ try {
35
+ return await res.json();
36
+ } catch {
37
+ const err = new Error(`Failed to parse JSON response from ${path} (HTTP ${res.status})`);
38
+ err.status = res.status;
39
+ throw err;
40
+ }
41
+ }
42
+
43
+ _buildUrl(path, params) {
44
+ let url = this.baseUrl + path;
45
+ if (params && typeof params === 'object') {
46
+ const qs = Object.entries(params)
47
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
48
+ .join('&');
49
+ if (qs) url += (url.includes('?') ? '&' : '?') + qs;
50
+ }
51
+ return url;
52
+ }
53
+
54
+ // Abort any in-flight request for the same cache key
55
+ abort(cacheKey) {
56
+ const controller = this.__activeRequests.get(cacheKey);
57
+ if (controller) {
58
+ controller.abort();
59
+ this.__activeRequests.delete(cacheKey);
60
+ }
61
+ }
62
+
63
+ // Unified request method — eliminates duplication across get/post/put/delete
64
+ async _request(method, path, opts = {}, body = undefined) {
65
+ const {
66
+ onSuccess, onError, cache = false, force = false, params = undefined,
67
+ debounce = 0, transform = undefined, headers = undefined, timeout = 30000,
68
+ } = opts;
69
+ const cacheKey = `${method}:${this._makeCacheKey(path, params)}`;
70
+
71
+ if (debounce > 0) {
72
+ return this._debounce(cacheKey, debounce, () =>
73
+ this._request(method, path, { ...opts, debounce: 0 }, body)
74
+ );
75
+ }
76
+
77
+ if (cache && !force && this.__cache.has(cacheKey)) {
78
+ const cached = this.__cache.get(cacheKey);
79
+ if (onSuccess) setTimeout(() => EventBus.trigger(onSuccess, cached), 0);
80
+ return cached;
81
+ }
82
+
83
+ const url = this._buildUrl(path, params);
84
+ const controller = new AbortController();
85
+ this.abort(cacheKey);
86
+ this.__activeRequests.set(cacheKey, controller);
87
+
88
+ const timeoutId = timeout > 0
89
+ ? setTimeout(() => controller.abort(), timeout)
90
+ : null;
91
+
92
+ const fetchOpts = {
93
+ method,
94
+ headers: {
95
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
96
+ ...this.headers,
97
+ ...headers,
98
+ },
99
+ signal: controller.signal,
100
+ };
101
+ if (body !== undefined) fetchOpts.body = JSON.stringify(body);
102
+
103
+ try {
104
+ const res = await fetch(url, fetchOpts);
105
+
106
+ // Validate HTTP status before parsing
107
+ if (!res.ok) {
108
+ const err = new Error(`HTTP ${res.status} ${res.statusText} on ${method} ${path}`);
109
+ err.status = res.status;
110
+ err.response = res;
111
+ throw err;
112
+ }
113
+
114
+ let data = await this._parseJson(res, path);
115
+ if (transform && typeof transform === 'function') {
116
+ data = transform(data);
117
+ }
118
+ if (cache) this.__cache.set(cacheKey, data);
119
+ if (onSuccess) EventBus.trigger(onSuccess, data);
120
+ return data;
121
+ } catch (err) {
122
+ if (onError) EventBus.trigger(onError, err);
123
+ throw err;
124
+ } finally {
125
+ if (timeoutId) clearTimeout(timeoutId);
126
+ this.__activeRequests.delete(cacheKey);
127
+ }
128
+ }
129
+
130
+ get(path, opts = {}) {
131
+ return this._request('GET', path, opts);
132
+ }
133
+
134
+ post(path, body, opts = {}) {
135
+ return this._request('POST', path, opts, body);
136
+ }
137
+
138
+ put(path, body, opts = {}) {
139
+ return this._request('PUT', path, opts, body);
140
+ }
141
+
142
+ delete(path, opts = {}) {
143
+ return this._request('DELETE', path, opts);
144
+ }
145
+ }
@@ -0,0 +1,65 @@
1
+ // src/core/defineElement.js
2
+ import { css, unsafeCSS } from 'lit';
3
+ import { injectServices } from './service.js';
4
+
5
+ // Convert PascalCase or camelCase to kebab-case
6
+ function toKebabCase(str) {
7
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Z])([A-Z][a-z])/g, '$1-$2').toLowerCase();
8
+ }
9
+
10
+ // Custom element tags must contain a hyphen and only valid characters
11
+ const VALID_CE_TAG = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
12
+
13
+ export function defineElement({ styles, tag, props, params, inject, template } = {}) {
14
+ return function (target) {
15
+ if (styles) {
16
+ const arr = Array.isArray(styles) ? styles : [styles];
17
+ target.styles = arr.map(s => {
18
+ if (typeof s === 'string') return css`${unsafeCSS(s)}`;
19
+ return s;
20
+ });
21
+ }
22
+
23
+ if (props)
24
+ target.properties = Object.assign({}, target.properties, props);
25
+
26
+ // Route params: register as Lit properties so they get default values
27
+ // and participate in the reactive update cycle.
28
+ if (params && typeof params === 'object') {
29
+ // Store the params schema so the routeParams setter can coerce types
30
+ target.__okalitParams = params;
31
+ target.properties = Object.assign({}, target.properties, params);
32
+ }
33
+
34
+ // Service injection support
35
+ if (inject && Array.isArray(inject)) {
36
+ const orig = target.prototype.connectedCallback;
37
+ target.prototype.connectedCallback = function () {
38
+ injectServices(this, inject);
39
+ if (orig) orig.call(this);
40
+ };
41
+ }
42
+
43
+ // Decoupled template: overrides the prototype's render method.
44
+ // When Lit calls this.render(), the context is the component instance.
45
+ if (template && typeof template === 'function') {
46
+ target.prototype.render = template;
47
+ }
48
+ // If no template is provided, Lit uses the render() defined in the class.
49
+
50
+ let finalTag = tag;
51
+ if (!finalTag)
52
+ finalTag = toKebabCase(target.name);
53
+
54
+ // Validate tag name to prevent malformed or dangerous custom element names
55
+ if (finalTag && !VALID_CE_TAG.test(finalTag)) {
56
+ console.error(`defineElement: invalid custom element tag "${finalTag}"`);
57
+ return target;
58
+ }
59
+
60
+ // Skip if already defined (prevents DOMException during Vite HMR)
61
+ if (finalTag && !window.customElements.get(finalTag)) {
62
+ window.customElements.define(finalTag, target);
63
+ }
64
+ };
65
+ }
@@ -0,0 +1,89 @@
1
+ import { EventBus } from './EventBus.js';
2
+ import { signal } from '@lit-labs/signals';
3
+
4
+ class OkalitI18nImpl {
5
+ constructor() {
6
+ this._translations = Object.create(null);
7
+ this._locale = signal('');
8
+ this._config = null;
9
+ this._ready = false;
10
+ }
11
+
12
+ async init(config) {
13
+ this._config = {
14
+ default: config.default || 'es',
15
+ locales: config.locales || [],
16
+ path: config.path || '/i18n',
17
+ };
18
+
19
+ const persisted = localStorage.getItem('okalit:i18n:locale');
20
+ const initialLocale = (persisted && this._config.locales.includes(persisted))
21
+ ? persisted
22
+ : this._config.default;
23
+
24
+ await this._loadLocale(initialLocale);
25
+ this._locale.set(initialLocale);
26
+ this._ready = true;
27
+
28
+ EventBus.emit('i18n:ready', { locale: initialLocale });
29
+
30
+ // Allow locale switching via EventBus
31
+ EventBus.listen('i18n:set-locale', ({ locale }) => this.setLocale(locale));
32
+ }
33
+
34
+ async _loadLocale(locale) {
35
+ if (this._translations[locale]) return;
36
+
37
+ const path = `${this._config.path}/${locale}.json`;
38
+ try {
39
+ const res = await fetch(path);
40
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
41
+ this._translations[locale] = await res.json();
42
+ } catch (e) {
43
+ console.error(`[OkalitI18n] Failed to load locale "${locale}" from ${path}:`, e);
44
+ this._translations[locale] = {};
45
+ }
46
+ }
47
+
48
+ async setLocale(locale) {
49
+ if (!this._config?.locales.includes(locale)) {
50
+ console.warn(`[OkalitI18n] Locale "${locale}" is not configured. Available: ${this._config?.locales}`);
51
+ return;
52
+ }
53
+
54
+ await this._loadLocale(locale);
55
+ this._locale.set(locale);
56
+ localStorage.setItem('okalit:i18n:locale', locale);
57
+ EventBus.emit('i18n:locale-changed', { locale });
58
+ }
59
+
60
+ get locale() {
61
+ return this._locale.get();
62
+ }
63
+
64
+ get locales() {
65
+ return this._config?.locales || [];
66
+ }
67
+
68
+ t(key, params) {
69
+ // Reading the signal value triggers SignalWatcher tracking
70
+ const locale = this._locale.get();
71
+ const dict = this._translations[locale] || {};
72
+
73
+ // Nested key support: "home.welcome.title" → dict.home.welcome.title
74
+ let value = key.split('.').reduce((obj, k) => obj?.[k], dict);
75
+
76
+ if (value === undefined) {
77
+ return key;
78
+ }
79
+
80
+ // Interpolation: "Hola {name}" + { name: "Alex" } → "Hola Alex"
81
+ if (params && typeof value === 'string') {
82
+ value = value.replace(/\{(\w+)\}/g, (_, k) => (k in params) ? params[k] : `{${k}}`);
83
+ }
84
+
85
+ return value;
86
+ }
87
+ }
88
+
89
+ export const OkalitI18n = new OkalitI18nImpl();
@@ -0,0 +1,40 @@
1
+ // src/core/idle.js
2
+ import { html } from 'lit';
3
+
4
+ export function idle({ template = () => html`<p>Loading...</p>`, dynamicLoader = [] } = {}) {
5
+ return (_, context) => {
6
+ context.addInitializer(function () {
7
+ const methodName = context.name;
8
+ const loaders = Array.isArray(dynamicLoader) ? dynamicLoader : [dynamicLoader];
9
+ this[`__${methodName}_loaded`] = false;
10
+ this[`__${methodName}_loading`] = false;
11
+
12
+ // Override the original method
13
+ const originalMethod = this[methodName];
14
+ this[methodName] = (...args) => {
15
+ if (this[`__${methodName}_loaded`]) {
16
+ return originalMethod.apply(this, args);
17
+ }
18
+ if (!this[`__${methodName}_loading`]) {
19
+ this[`__${methodName}_loading`] = true;
20
+ const load = () => {
21
+ Promise.all(loaders.map(fn => fn())).then(() => {
22
+ this[`__${methodName}_loaded`] = true;
23
+ this.requestUpdate && this.requestUpdate();
24
+ }).catch(err => {
25
+ this[`__${methodName}_loading`] = false;
26
+ console.error(`idle("${methodName}"): loader failed`, err);
27
+ });
28
+ };
29
+ if ('requestIdleCallback' in window) {
30
+ window.requestIdleCallback(load);
31
+ } else {
32
+ // Fallback when requestIdleCallback is not available
33
+ setTimeout(load, 200);
34
+ }
35
+ }
36
+ return template();
37
+ };
38
+ });
39
+ };
40
+ }
@@ -0,0 +1,10 @@
1
+ export { Okalit } from './Okalit.js';
2
+ export { defineElement } from './defineElement.js';
3
+ export { AppMixin } from './AppMixin.js';
4
+ export { ModuleMixin } from './ModuleMixin.js';
5
+ export { OkalitService } from './OkalitService.js';
6
+ export { idle } from './idle.js';
7
+ export { lazy} from './lazy.js';
8
+ export { when } from './when.js';
9
+ export { service, getService, injectServices } from './service.js';
10
+ export { OkalitI18n } from './i18n.js';
@@ -0,0 +1,32 @@
1
+ // src/core/lazy.js
2
+ import { html } from 'lit';
3
+
4
+ export function lazy({ template = () => html`<p>Loading...</p>`, dynamicLoader = [] } = {}) {
5
+ return (_, context) => {
6
+ context.addInitializer(function () {
7
+ const methodName = context.name;
8
+ const loaders = Array.isArray(dynamicLoader) ? dynamicLoader : [dynamicLoader];
9
+ this[`__${methodName}_loaded`] = false;
10
+ this[`__${methodName}_loading`] = false;
11
+
12
+ // Override the original method
13
+ const originalMethod = this[methodName];
14
+ this[methodName] = (...args) => {
15
+ if (this[`__${methodName}_loaded`]) {
16
+ return originalMethod.apply(this, args);
17
+ }
18
+ if (!this[`__${methodName}_loading`]) {
19
+ this[`__${methodName}_loading`] = true;
20
+ Promise.all(loaders.map(fn => fn())).then(() => {
21
+ this[`__${methodName}_loaded`] = true;
22
+ this.requestUpdate && this.requestUpdate();
23
+ }).catch(err => {
24
+ this[`__${methodName}_loading`] = false;
25
+ console.error(`lazy("${methodName}"): loader failed`, err);
26
+ });
27
+ }
28
+ return template();
29
+ };
30
+ });
31
+ };
32
+ }