@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.
- package/README.md +32 -0
- package/bin/okalit-cli.js +8 -0
- package/lib/cli.js +515 -0
- package/package.json +30 -0
- package/templates/app/@okalit/AppMixin.js +29 -0
- package/templates/app/@okalit/EventBus.js +152 -0
- package/templates/app/@okalit/ModuleMixin.js +7 -0
- package/templates/app/@okalit/Okalit.js +129 -0
- package/templates/app/@okalit/OkalitService.js +145 -0
- package/templates/app/@okalit/defineElement.js +65 -0
- package/templates/app/@okalit/i18n.js +89 -0
- package/templates/app/@okalit/idle.js +40 -0
- package/templates/app/@okalit/index.js +10 -0
- package/templates/app/@okalit/lazy.js +32 -0
- package/templates/app/@okalit/okalit-router.js +309 -0
- package/templates/app/@okalit/service.js +33 -0
- package/templates/app/@okalit/trigger.js +14 -0
- package/templates/app/@okalit/viewport.js +69 -0
- package/templates/app/@okalit/when.js +40 -0
- package/templates/app/babel.config.json +5 -0
- package/templates/app/index.html +15 -0
- package/templates/app/package.json +23 -0
- package/templates/app/public/i18n/en.json +3 -0
- package/templates/app/public/i18n/es.json +3 -0
- package/templates/app/public/lit.svg +1 -0
- package/templates/app/src/app.routes.ts +10 -0
- package/templates/app/src/main-app.js +13 -0
- package/templates/app/src/modules/example/example.module.js +4 -0
- package/templates/app/src/modules/example/example.routes.js +7 -0
- package/templates/app/src/modules/example/pages/example.page.js +43 -0
- package/templates/app/src/modules/example/pages/example.page.scss +76 -0
- package/templates/app/src/styles/global.scss +0 -0
- package/templates/app/src/styles/index.css +4 -0
- package/templates/app/vite.config.js +19 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { html as staticHtml, unsafeStatic } from 'lit/static-html.js';
|
|
3
|
+
import { EventBus } from './EventBus.js';
|
|
4
|
+
|
|
5
|
+
// Valid custom element tag: lowercase, must contain at least one hyphen
|
|
6
|
+
const VALID_CE_TAG = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
|
7
|
+
|
|
8
|
+
// Normalize a URL path: collapse repeated slashes, strip trailing slash
|
|
9
|
+
function sanitizePath(path) {
|
|
10
|
+
let p = path.replace(/\/+/g, '/');
|
|
11
|
+
if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
|
|
12
|
+
return p;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Parse query string into a plain object
|
|
16
|
+
function parseQueryString(search) {
|
|
17
|
+
const params = {};
|
|
18
|
+
if (!search) return params;
|
|
19
|
+
const qs = search.startsWith('?') ? search.slice(1) : search;
|
|
20
|
+
if (!qs) return params;
|
|
21
|
+
for (const pair of qs.split('&')) {
|
|
22
|
+
const idx = pair.indexOf('=');
|
|
23
|
+
if (idx === -1) continue;
|
|
24
|
+
const key = decodeURIComponent(pair.slice(0, idx));
|
|
25
|
+
const val = decodeURIComponent(pair.slice(idx + 1));
|
|
26
|
+
params[key] = val;
|
|
27
|
+
}
|
|
28
|
+
return params;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Try to match a single route pattern (which may contain :param segments)
|
|
32
|
+
// against an actual path. Returns extracted params or null on mismatch.
|
|
33
|
+
function matchSegments(pattern, path) {
|
|
34
|
+
const patternParts = pattern.split('/').filter(Boolean);
|
|
35
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
36
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
37
|
+
const params = {};
|
|
38
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
39
|
+
if (patternParts[i].startsWith(':')) {
|
|
40
|
+
params[patternParts[i].slice(1)] = pathParts[i];
|
|
41
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return params;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function matchRoute(path, routes, basePath = '') {
|
|
49
|
+
for (const route of routes) {
|
|
50
|
+
let fullPath = basePath;
|
|
51
|
+
if (route.path && route.path !== '') {
|
|
52
|
+
if (!fullPath.endsWith('/')) fullPath += '/';
|
|
53
|
+
fullPath += route.path.replace(/^\//, '');
|
|
54
|
+
}
|
|
55
|
+
if (fullPath.length > 1 && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1);
|
|
56
|
+
|
|
57
|
+
// Try exact match first, then pattern match for :param segments
|
|
58
|
+
const isExact = fullPath === path;
|
|
59
|
+
const segmentParams = !isExact ? matchSegments(fullPath, path) : {};
|
|
60
|
+
|
|
61
|
+
if (isExact || segmentParams) {
|
|
62
|
+
if (route.children) {
|
|
63
|
+
const match = matchRoute(path, route.children, fullPath);
|
|
64
|
+
if (match) {
|
|
65
|
+
return {
|
|
66
|
+
route: match.route,
|
|
67
|
+
parents: [route, ...match.parents],
|
|
68
|
+
params: { ...segmentParams, ...match.params }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const defaultChild = route.children.find(c => c.path === '');
|
|
72
|
+
if (defaultChild) {
|
|
73
|
+
return { route: defaultChild, parents: [route], params: segmentParams || {} };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { route, parents: [], params: segmentParams || {} };
|
|
77
|
+
}
|
|
78
|
+
if (route.children) {
|
|
79
|
+
const match = matchRoute(path, route.children, fullPath);
|
|
80
|
+
if (match) {
|
|
81
|
+
return {
|
|
82
|
+
route: match.route,
|
|
83
|
+
parents: [route, ...match.parents],
|
|
84
|
+
params: match.params
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class OkalitRouter extends LitElement {
|
|
93
|
+
static properties = {
|
|
94
|
+
routes: { type: Array },
|
|
95
|
+
guards: { type: Array },
|
|
96
|
+
interceptors: { type: Array },
|
|
97
|
+
currentRoute: { type: Object },
|
|
98
|
+
params: { type: Object },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
super();
|
|
103
|
+
this._guards = [];
|
|
104
|
+
this._interceptors = [];
|
|
105
|
+
this._routes = [];
|
|
106
|
+
this.currentRoute = null;
|
|
107
|
+
this.params = {};
|
|
108
|
+
this._listenBus();
|
|
109
|
+
this._boundPopState = () => this._onPopState();
|
|
110
|
+
window.addEventListener('popstate', this._boundPopState);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
disconnectedCallback() {
|
|
114
|
+
super.disconnectedCallback();
|
|
115
|
+
window.removeEventListener('popstate', this._boundPopState);
|
|
116
|
+
if (this._busUnsubs) {
|
|
117
|
+
this._busUnsubs.forEach(unsub => unsub());
|
|
118
|
+
this._busUnsubs = [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Runs once after the component has rendered and received all initial
|
|
124
|
+
* properties (routes, guards, etc.)
|
|
125
|
+
*/
|
|
126
|
+
firstUpdated() {
|
|
127
|
+
this._navigate(window.location.pathname);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
set routes(val) {
|
|
131
|
+
this._routes = val;
|
|
132
|
+
// Only auto-navigate if the component has already been initially rendered.
|
|
133
|
+
// On first load, firstUpdated() handles navigation.
|
|
134
|
+
if (this.hasUpdated) {
|
|
135
|
+
this._navigate(window.location.pathname);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
get routes() {
|
|
139
|
+
return this._routes;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
set guards(val) {
|
|
143
|
+
this._guards = Array.isArray(val) ? val : [];
|
|
144
|
+
}
|
|
145
|
+
get guards() {
|
|
146
|
+
return this._guards;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
set interceptors(val) {
|
|
150
|
+
this._interceptors = Array.isArray(val) ? val : [];
|
|
151
|
+
}
|
|
152
|
+
get interceptors() {
|
|
153
|
+
return this._interceptors;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_listenBus() {
|
|
157
|
+
this._busUnsubs = [];
|
|
158
|
+
this._busUnsubs.push(
|
|
159
|
+
EventBus.listen('okalit-route:navigate', ({ path, args }) => {
|
|
160
|
+
this.navigate(path, args);
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
this._busUnsubs.push(
|
|
164
|
+
EventBus.listen('okalit-route:back', () => {
|
|
165
|
+
window.history.back();
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async _runGuards(guardsList, path, args) {
|
|
171
|
+
const list = guardsList || [];
|
|
172
|
+
for (const guard of list) {
|
|
173
|
+
const result = await guard(path, args);
|
|
174
|
+
|
|
175
|
+
// Support { allow: false, redirect: '...' }
|
|
176
|
+
if (result && typeof result === 'object' && result.allow === false) {
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Legacy support: return false
|
|
181
|
+
if (result === false) {
|
|
182
|
+
return { allow: false };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { allow: true };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async navigate(path, args) {
|
|
189
|
+
const safePath = sanitizePath(path);
|
|
190
|
+
const success = await this._navigate(safePath, args);
|
|
191
|
+
if (success) {
|
|
192
|
+
// Preserve query string in the URL if present
|
|
193
|
+
const qsIdx = path.indexOf('?');
|
|
194
|
+
const fullUrl = qsIdx !== -1 ? safePath + path.slice(qsIdx) : safePath;
|
|
195
|
+
window.history.pushState({}, '', fullUrl);
|
|
196
|
+
EventBus.clearAll({ persist: 'memory' });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async _navigate(path, args) {
|
|
201
|
+
const safePath = sanitizePath(path);
|
|
202
|
+
|
|
203
|
+
// 1. Global guards
|
|
204
|
+
const globalCheck = await this._runGuards(this._guards, safePath, args);
|
|
205
|
+
if (!globalCheck.allow) {
|
|
206
|
+
if (globalCheck.redirect) {
|
|
207
|
+
this.navigate(globalCheck.redirect);
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2. Route matching (extracts :param values from path segments)
|
|
213
|
+
const match = matchRoute(safePath, this.routes);
|
|
214
|
+
if (!match) {
|
|
215
|
+
this.currentRoute = null;
|
|
216
|
+
this.parents = [];
|
|
217
|
+
this.params = {};
|
|
218
|
+
this.requestUpdate();
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { route, parents, params: routeParams } = match;
|
|
223
|
+
|
|
224
|
+
// 3. Merge route params with query string params.
|
|
225
|
+
// Route params (:id) take priority over query params (?id=).
|
|
226
|
+
const queryParams = parseQueryString(window.location.search);
|
|
227
|
+
this.params = { ...queryParams, ...routeParams };
|
|
228
|
+
|
|
229
|
+
// 4. Route-level guards
|
|
230
|
+
if (route.guard) {
|
|
231
|
+
const routeCheck = await this._runGuards([route.guard], safePath, args);
|
|
232
|
+
if (!routeCheck.allow) {
|
|
233
|
+
if (routeCheck.redirect) {
|
|
234
|
+
this.navigate(routeCheck.redirect);
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 5. Dynamic imports (only after guards pass).
|
|
241
|
+
// parents is already ordered outermost-first from matchRoute.
|
|
242
|
+
for (const r of [...parents, route]) {
|
|
243
|
+
if (r.import) await r.import();
|
|
244
|
+
if (r.childrens && !r.children) {
|
|
245
|
+
const mod = await r.childrens();
|
|
246
|
+
r.children = mod.default || mod.routes || mod;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.currentRoute = route;
|
|
251
|
+
this.parents = parents;
|
|
252
|
+
this.requestUpdate();
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_onPopState() {
|
|
257
|
+
this._navigate(window.location.pathname);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
addGuard(guard) {
|
|
261
|
+
this._guards.push(guard);
|
|
262
|
+
}
|
|
263
|
+
addInterceptor(interceptor) {
|
|
264
|
+
this._interceptors.push(interceptor);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_renderNested(parents, route) {
|
|
268
|
+
if (!route) return staticHtml``;
|
|
269
|
+
const tag = route.component;
|
|
270
|
+
|
|
271
|
+
// Validate custom element tag to prevent injection via unsafeStatic
|
|
272
|
+
if (!VALID_CE_TAG.test(tag)) {
|
|
273
|
+
console.error(`OkalitRouter: invalid component tag "${tag}"`);
|
|
274
|
+
return staticHtml``;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (parents.length === 0) {
|
|
278
|
+
return staticHtml`<${unsafeStatic(tag)}></${unsafeStatic(tag)}>`;
|
|
279
|
+
}
|
|
280
|
+
const [parent, ...rest] = parents;
|
|
281
|
+
const parentTag = parent.component;
|
|
282
|
+
|
|
283
|
+
if (!VALID_CE_TAG.test(parentTag)) {
|
|
284
|
+
console.error(`OkalitRouter: invalid component tag "${parentTag}"`);
|
|
285
|
+
return staticHtml``;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return staticHtml`<${unsafeStatic(parentTag)}>
|
|
289
|
+
${this._renderNested(rest, route)}
|
|
290
|
+
</${unsafeStatic(parentTag)}>`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// After each render, find the leaf component and inject route params
|
|
294
|
+
updated() {
|
|
295
|
+
if (!this.currentRoute || !this.params) return;
|
|
296
|
+
const tag = this.currentRoute.component;
|
|
297
|
+
const leaf = this.renderRoot.querySelector(tag);
|
|
298
|
+
if (leaf && typeof leaf === 'object') {
|
|
299
|
+
leaf.routeParams = this.params;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
render() {
|
|
304
|
+
if (!this.currentRoute) return staticHtml``;
|
|
305
|
+
return this._renderNested(this.parents || [], this.currentRoute);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
customElements.define('okalit-router', OkalitRouter);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/core/service.js
|
|
2
|
+
// Vanilla JS decorator for service registration and DI
|
|
3
|
+
|
|
4
|
+
const __serviceRegistry = new Map();
|
|
5
|
+
|
|
6
|
+
export function service(name) {
|
|
7
|
+
return function (target) {
|
|
8
|
+
if (__serviceRegistry.has(name)) {
|
|
9
|
+
console.warn(`service("${name}"): already registered — skipping duplicate.`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
__serviceRegistry.set(name, new target());
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getService(name) {
|
|
17
|
+
return __serviceRegistry.get(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Helper for DI in components
|
|
21
|
+
export function injectServices(target, injectList) {
|
|
22
|
+
if (!Array.isArray(injectList)) return;
|
|
23
|
+
injectList.forEach((name) => {
|
|
24
|
+
const instance = getService(name);
|
|
25
|
+
if (instance) {
|
|
26
|
+
Object.defineProperty(target, name, {
|
|
27
|
+
value: instance,
|
|
28
|
+
writable: false,
|
|
29
|
+
configurable: false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/core/trigger.js
|
|
2
|
+
import { EventBus } from './EventBus.js';
|
|
3
|
+
|
|
4
|
+
export function trigger(event) {
|
|
5
|
+
return (target, context) => {
|
|
6
|
+
context.addInitializer(function () {
|
|
7
|
+
// Subscribe the decorated method to the global trigger
|
|
8
|
+
this._okalit_trigger_unsubs = this._okalit_trigger_unsubs || [];
|
|
9
|
+
const unsub = EventBus.listen(event, this[context.name].bind(this));
|
|
10
|
+
this._okalit_trigger_unsubs.push(unsub);
|
|
11
|
+
});
|
|
12
|
+
// Cleanup is handled centrally in Okalit.disconnectedCallback
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/core/viewport.js
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
|
|
4
|
+
export function viewport({ template = () => html`<p>Loading...</p>`, dynamicLoader = [] } = {}) {
|
|
5
|
+
return (target, 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
|
+
this[`__${methodName}_observer`] = null;
|
|
12
|
+
|
|
13
|
+
// Override the original method
|
|
14
|
+
const originalMethod = this[methodName];
|
|
15
|
+
this[methodName] = (...args) => {
|
|
16
|
+
if (this[`__${methodName}_loaded`]) {
|
|
17
|
+
return originalMethod.apply(this, args);
|
|
18
|
+
}
|
|
19
|
+
if (!this[`__${methodName}_loading`]) {
|
|
20
|
+
this[`__${methodName}_loading`] = true;
|
|
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(`viewport("${methodName}"): loader failed`, err);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return template();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Set up IntersectionObserver to trigger loading on visibility
|
|
33
|
+
const setupObserver = () => {
|
|
34
|
+
if (this[`__${methodName}_observer`]) return;
|
|
35
|
+
this[`__${methodName}_observer`] = new IntersectionObserver(entries => {
|
|
36
|
+
if (entries[0].isIntersecting) {
|
|
37
|
+
this[`__${methodName}_observer`].disconnect();
|
|
38
|
+
this[`__${methodName}_observer`] = null;
|
|
39
|
+
this[methodName](); // Trigger load
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Wait for the component to be in the DOM
|
|
43
|
+
requestAnimationFrame(() => {
|
|
44
|
+
const el = this.renderRoot?.querySelector(`[data-viewport="${methodName}"]`);
|
|
45
|
+
if (el) {
|
|
46
|
+
this[`__${methodName}_observer`].observe(el);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Attach observer on first render
|
|
52
|
+
const origFirstUpdated = this.firstUpdated;
|
|
53
|
+
this.firstUpdated = function (...args) {
|
|
54
|
+
setupObserver.call(this);
|
|
55
|
+
origFirstUpdated && origFirstUpdated.apply(this, args);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Clean up observer on disconnect to prevent memory leaks
|
|
59
|
+
const origDisconnected = this.disconnectedCallback;
|
|
60
|
+
this.disconnectedCallback = function (...args) {
|
|
61
|
+
if (this[`__${methodName}_observer`]) {
|
|
62
|
+
this[`__${methodName}_observer`].disconnect();
|
|
63
|
+
this[`__${methodName}_observer`] = null;
|
|
64
|
+
}
|
|
65
|
+
origDisconnected && origDisconnected.apply(this, args);
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/core/when.js
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
|
|
4
|
+
export function when({ template = () => html`<p>Loading...</p>`, dynamicLoader = [], condition = () => false } = {}) {
|
|
5
|
+
return (target, 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 condition is false, reset state and show the placeholder
|
|
16
|
+
if (!condition.call(this)) {
|
|
17
|
+
this[`__${methodName}_loaded`] = false;
|
|
18
|
+
this[`__${methodName}_loading`] = false;
|
|
19
|
+
return template();
|
|
20
|
+
}
|
|
21
|
+
// Already loaded — render original content
|
|
22
|
+
if (this[`__${methodName}_loaded`]) {
|
|
23
|
+
return originalMethod.apply(this, args);
|
|
24
|
+
}
|
|
25
|
+
// Start loading
|
|
26
|
+
if (!this[`__${methodName}_loading`]) {
|
|
27
|
+
this[`__${methodName}_loading`] = true;
|
|
28
|
+
Promise.all(loaders.map(fn => fn())).then(() => {
|
|
29
|
+
this[`__${methodName}_loaded`] = true;
|
|
30
|
+
this.requestUpdate && this.requestUpdate();
|
|
31
|
+
}).catch(err => {
|
|
32
|
+
this[`__${methodName}_loading`] = false;
|
|
33
|
+
console.error(`when("${methodName}"): loader failed`, err);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return template();
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/lit.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
|
8
|
+
<title>Okalit App</title>
|
|
9
|
+
<link rel="stylesheet" href="./src/styles/index.css" />
|
|
10
|
+
<script type="module" src="/src/main-app.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main-app></main-app>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "okalit-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@lit-labs/signals": "^0.2.0",
|
|
13
|
+
"lit": "^3.3.1"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@babel/core": "^7.29.0",
|
|
17
|
+
"@babel/plugin-proposal-decorators": "^7.29.0",
|
|
18
|
+
"@babel/preset-env": "^7.29.0",
|
|
19
|
+
"sass": "^1.97.3",
|
|
20
|
+
"vite": "^7.3.1",
|
|
21
|
+
"vite-plugin-babel": "^1.5.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="25.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 320"><path fill="#00E8FF" d="m64 192l25.926-44.727l38.233-19.114l63.974 63.974l10.833 61.754L192 320l-64-64l-38.074-25.615z"></path><path fill="#283198" d="M128 256V128l64-64v128l-64 64ZM0 256l64 64l9.202-60.602L64 192l-37.542 23.71L0 256Z"></path><path fill="#324FFF" d="M64 192V64l64-64v128l-64 64Zm128 128V192l64-64v128l-64 64ZM0 256V128l64 64l-64 64Z"></path><path fill="#0FF" d="M64 320V192l64 64z"></path></svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Okalit, defineElement, AppMixin } from '@okalit';
|
|
2
|
+
import routes from './app.routes.ts';
|
|
3
|
+
|
|
4
|
+
@defineElement({ tag: 'main-app' })
|
|
5
|
+
export class MainApp extends AppMixin(Okalit) {
|
|
6
|
+
_appConfig = {
|
|
7
|
+
routes,
|
|
8
|
+
i18n: {
|
|
9
|
+
default: 'es',
|
|
10
|
+
locales: ['es', 'en'],
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { html } from "lit";
|
|
2
|
+
import { Okalit, defineElement } from "@okalit";
|
|
3
|
+
|
|
4
|
+
import styles from "./example.page.scss?inline";
|
|
5
|
+
import global from "@styles/global.scss?inline";
|
|
6
|
+
|
|
7
|
+
@defineElement({
|
|
8
|
+
tag: "example-page",
|
|
9
|
+
styles: [styles, global]
|
|
10
|
+
})
|
|
11
|
+
export class ExamplePage extends Okalit {
|
|
12
|
+
counter = this.channel('demo:counter', {
|
|
13
|
+
initialValue: 0
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
increment() {
|
|
17
|
+
this.emit('demo:counter', this.counter.get() + 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
decrement() {
|
|
21
|
+
this.emit('demo:counter', this.counter.get() - 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
return html`
|
|
26
|
+
<div class="counter-card">
|
|
27
|
+
<h1>${this.t('WELCOME')}</h1>
|
|
28
|
+
<p>${this.t('DESCRIPTION')}</p>
|
|
29
|
+
|
|
30
|
+
<span class="count-display">${this.counter.get()}</span>
|
|
31
|
+
|
|
32
|
+
<div class="controls">
|
|
33
|
+
<button class="btn-decrement" @click="${this.decrement}">
|
|
34
|
+
${this.t('DECREMENT')}
|
|
35
|
+
</button>
|
|
36
|
+
<button class="btn-increment" @click="${this.increment}">
|
|
37
|
+
${this.t('INCREMENT')}
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: flex;
|
|
3
|
+
justify-content: center;
|
|
4
|
+
align-items: center;
|
|
5
|
+
min-height: 100vh;
|
|
6
|
+
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
7
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
8
|
+
color: #333;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.counter-card {
|
|
12
|
+
background: white;
|
|
13
|
+
padding: 3rem;
|
|
14
|
+
border-radius: 20px;
|
|
15
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
16
|
+
text-align: center;
|
|
17
|
+
transition: transform 0.2s ease;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.counter-card:hover {
|
|
21
|
+
transform: translateY(-5px);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
h1 {
|
|
25
|
+
margin: 0 0 1rem 0;
|
|
26
|
+
font-size: 2rem;
|
|
27
|
+
color: #4a5568;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
p {
|
|
31
|
+
color: #718096;
|
|
32
|
+
margin-bottom: 2rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.count-display {
|
|
36
|
+
font-size: 4rem;
|
|
37
|
+
font-weight: bold;
|
|
38
|
+
color: #5a67d8;
|
|
39
|
+
margin-bottom: 2rem;
|
|
40
|
+
display: block;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.controls {
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: 1rem;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
button {
|
|
50
|
+
padding: 10px 20px;
|
|
51
|
+
font-size: 1rem;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
border: none;
|
|
54
|
+
border-radius: 10px;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
transition: all 0.2s ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.btn-decrement {
|
|
60
|
+
background-color: #edf2f7;
|
|
61
|
+
color: #4a5568;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.btn-decrement:hover {
|
|
65
|
+
background-color: #e2e8f0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.btn-increment {
|
|
69
|
+
background-color: #5a67d8;
|
|
70
|
+
color: white;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.btn-increment:hover {
|
|
74
|
+
background-color: #4c51bf;
|
|
75
|
+
box-shadow: 0 4px 12px rgba(90, 103, 216, 0.3);
|
|
76
|
+
}
|
|
File without changes
|