@okalit/cli 0.1.0 → 0.2.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 +13 -9
- package/lib/cli.js +38 -30
- package/package.json +1 -1
- package/templates/app/@okalit/Okalit.js +163 -89
- package/templates/app/@okalit/channel.js +177 -0
- package/templates/app/@okalit/define-element.js +30 -0
- package/templates/app/@okalit/i18n.js +88 -53
- package/templates/app/@okalit/index.js +8 -10
- package/templates/app/@okalit/mixins.js +106 -0
- package/templates/app/@okalit/performance.js +211 -0
- package/templates/app/@okalit/router-outlet.js +66 -0
- package/templates/app/@okalit/router.js +240 -0
- package/templates/app/@okalit/service.js +318 -23
- package/templates/app/index.html +0 -1
- package/templates/app/package.json +2 -3
- package/templates/app/public/i18n/en.json +47 -1
- package/templates/app/public/i18n/es.json +47 -1
- package/templates/app/src/{app.routes.ts → app.routes.js} +1 -1
- package/templates/app/src/channels/example.channel.js +15 -0
- package/templates/app/src/components/lazy-widget.js +13 -0
- package/templates/app/src/guards/auth.guard.js +17 -0
- package/templates/app/src/layouts/app-layout.js +75 -0
- package/templates/app/src/main-app.js +19 -9
- package/templates/app/src/modules/example/example.module.js +8 -3
- package/templates/app/src/modules/example/example.routes.js +27 -4
- package/templates/app/src/modules/example/pages/detail/example-detail.js +21 -0
- package/templates/app/src/modules/example/pages/home/example-home.js +39 -0
- package/templates/app/src/modules/example/pages/lifecycle/example-lifecycle.js +74 -0
- package/templates/app/src/modules/example/pages/performance/example-performance.js +59 -0
- package/templates/app/src/modules/example/pages/services/example-services.js +80 -0
- package/templates/app/src/services/rickandmorty.service.js +17 -0
- package/templates/app/src/services/user.service.js +33 -0
- package/templates/app/src/styles/global.scss +250 -0
- package/templates/app/src/styles/index.css +11 -2
- package/templates/app/vite.config.js +2 -0
- package/templates/app/@okalit/AppMixin.js +0 -29
- package/templates/app/@okalit/EventBus.js +0 -152
- package/templates/app/@okalit/ModuleMixin.js +0 -7
- package/templates/app/@okalit/OkalitService.js +0 -145
- package/templates/app/@okalit/defineElement.js +0 -65
- package/templates/app/@okalit/idle.js +0 -40
- package/templates/app/@okalit/lazy.js +0 -32
- package/templates/app/@okalit/okalit-router.js +0 -309
- package/templates/app/@okalit/trigger.js +0 -14
- package/templates/app/@okalit/viewport.js +0 -69
- package/templates/app/@okalit/when.js +0 -40
- package/templates/app/public/lit.svg +0 -1
- package/templates/app/src/modules/example/pages/example.page.js +0 -43
- package/templates/app/src/modules/example/pages/example.page.scss +0 -76
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
4
|
+
color: #1a1a1a;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// ── App shell ─────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
.app-header {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
padding: 16px 32px;
|
|
14
|
+
border-bottom: 1px solid #eee;
|
|
15
|
+
gap: 24px;
|
|
16
|
+
}
|
|
17
|
+
.app-brand {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
h1 {
|
|
21
|
+
font-size: 1.1rem;
|
|
22
|
+
font-weight: 700;
|
|
23
|
+
margin: 0;
|
|
24
|
+
color: #1a1a1a;
|
|
25
|
+
letter-spacing: -0.02em;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
.app-subtitle {
|
|
29
|
+
font-size: 0.75rem;
|
|
30
|
+
color: #999;
|
|
31
|
+
}
|
|
32
|
+
.app-nav {
|
|
33
|
+
display: flex;
|
|
34
|
+
gap: 8px;
|
|
35
|
+
a {
|
|
36
|
+
padding: 6px 14px;
|
|
37
|
+
border-radius: 6px;
|
|
38
|
+
font-size: 0.85rem;
|
|
39
|
+
font-weight: 500;
|
|
40
|
+
color: #555;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
transition: background 0.15s, color 0.15s;
|
|
43
|
+
&:hover {
|
|
44
|
+
background: #f5f5f5;
|
|
45
|
+
color: #1a1a1a;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
.locale-switch {
|
|
50
|
+
display: flex;
|
|
51
|
+
gap: 4px;
|
|
52
|
+
button {
|
|
53
|
+
padding: 4px 10px;
|
|
54
|
+
border: 1px solid #ddd;
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
background: white;
|
|
57
|
+
font-size: 0.75rem;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
color: #888;
|
|
61
|
+
&.active {
|
|
62
|
+
background: #1a1a1a;
|
|
63
|
+
color: white;
|
|
64
|
+
border-color: #1a1a1a;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
.app-content {
|
|
69
|
+
padding: 32px;
|
|
70
|
+
max-width: 900px;
|
|
71
|
+
margin: 0 auto;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Typography ────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
h2 {
|
|
77
|
+
font-size: 1.5rem;
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
margin: 0 0 8px;
|
|
80
|
+
letter-spacing: -0.02em;
|
|
81
|
+
}
|
|
82
|
+
h3 {
|
|
83
|
+
font-size: 1.1rem;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
margin: 24px 0 8px;
|
|
86
|
+
color: #333;
|
|
87
|
+
}
|
|
88
|
+
p {
|
|
89
|
+
margin: 0 0 12px;
|
|
90
|
+
line-height: 1.6;
|
|
91
|
+
color: #555;
|
|
92
|
+
}
|
|
93
|
+
.hint {
|
|
94
|
+
font-size: 0.8rem;
|
|
95
|
+
color: #aaa;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Cards ─────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
.card {
|
|
101
|
+
background: #fff;
|
|
102
|
+
border: 1px solid #eee;
|
|
103
|
+
border-radius: 12px;
|
|
104
|
+
padding: 24px;
|
|
105
|
+
margin-bottom: 16px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Buttons ───────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
button {
|
|
111
|
+
padding: 8px 16px;
|
|
112
|
+
border: 1px solid #ddd;
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
background: white;
|
|
115
|
+
font-size: 0.85rem;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
transition: all 0.12s ease;
|
|
119
|
+
color: #333;
|
|
120
|
+
|
|
121
|
+
&:hover {
|
|
122
|
+
border-color: #bbb;
|
|
123
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
124
|
+
}
|
|
125
|
+
&:active {
|
|
126
|
+
transform: scale(0.97);
|
|
127
|
+
}
|
|
128
|
+
&.primary {
|
|
129
|
+
background: #1a1a1a;
|
|
130
|
+
color: white;
|
|
131
|
+
border-color: #1a1a1a;
|
|
132
|
+
}
|
|
133
|
+
&.danger {
|
|
134
|
+
background: #ff4444;
|
|
135
|
+
color: white;
|
|
136
|
+
border-color: #ff4444;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Counter display ───────────────────────────────────
|
|
141
|
+
|
|
142
|
+
.count {
|
|
143
|
+
font-size: 4rem;
|
|
144
|
+
font-weight: 800;
|
|
145
|
+
line-height: 1;
|
|
146
|
+
text-align: center;
|
|
147
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
148
|
+
-webkit-background-clip: text;
|
|
149
|
+
-webkit-text-fill-color: transparent;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Controls rows ─────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
.controls-row {
|
|
155
|
+
display: flex;
|
|
156
|
+
gap: 8px;
|
|
157
|
+
flex-wrap: wrap;
|
|
158
|
+
align-items: center;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Log panel ─────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
.log-panel {
|
|
164
|
+
background: #fafafa;
|
|
165
|
+
border: 1px solid #eee;
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
padding: 16px;
|
|
168
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
169
|
+
font-size: 0.8rem;
|
|
170
|
+
max-height: 240px;
|
|
171
|
+
overflow-y: auto;
|
|
172
|
+
line-height: 1.8;
|
|
173
|
+
.log-entry {
|
|
174
|
+
color: #555;
|
|
175
|
+
&::before { content: '› '; color: #bbb; }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Lists ─────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
ul.data-list {
|
|
182
|
+
list-style: none;
|
|
183
|
+
padding: 0;
|
|
184
|
+
margin: 0;
|
|
185
|
+
li {
|
|
186
|
+
padding: 8px 0;
|
|
187
|
+
border-bottom: 1px solid #f0f0f0;
|
|
188
|
+
font-size: 0.9rem;
|
|
189
|
+
color: #444;
|
|
190
|
+
&:last-child { border-bottom: none; }
|
|
191
|
+
strong { color: #1a1a1a; }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Fallback / loading ────────────────────────────────
|
|
196
|
+
|
|
197
|
+
.fallback {
|
|
198
|
+
padding: 16px;
|
|
199
|
+
text-align: center;
|
|
200
|
+
color: #aaa;
|
|
201
|
+
font-size: 0.85rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Spacer ────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
.spacer {
|
|
207
|
+
height: 600px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Footer (Interceptor log) ──────────────────────────
|
|
211
|
+
|
|
212
|
+
.app-footer {
|
|
213
|
+
position: fixed;
|
|
214
|
+
bottom: 0;
|
|
215
|
+
left: 0;
|
|
216
|
+
right: 0;
|
|
217
|
+
z-index: 100;
|
|
218
|
+
background: white;
|
|
219
|
+
border-top: 1px solid #eee;
|
|
220
|
+
}
|
|
221
|
+
.footer-bar {
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
gap: 8px;
|
|
225
|
+
padding: 8px 32px;
|
|
226
|
+
}
|
|
227
|
+
.footer-toggle {
|
|
228
|
+
font-size: 0.75rem;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
padding: 4px 12px;
|
|
231
|
+
border: 1px solid #ddd;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
background: #fafafa;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
color: #666;
|
|
236
|
+
&:hover { background: #f0f0f0; }
|
|
237
|
+
}
|
|
238
|
+
.footer-clear {
|
|
239
|
+
font-size: 0.7rem;
|
|
240
|
+
padding: 3px 8px;
|
|
241
|
+
border: none;
|
|
242
|
+
background: none;
|
|
243
|
+
color: #aaa;
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
&:hover { color: #555; }
|
|
246
|
+
}
|
|
247
|
+
.footer-log {
|
|
248
|
+
margin: 0 32px 8px;
|
|
249
|
+
max-height: 160px;
|
|
250
|
+
}
|
|
@@ -14,6 +14,8 @@ export default defineConfig({
|
|
|
14
14
|
'@styles': path.resolve(__dirname, 'src/styles'),
|
|
15
15
|
'@guards': path.resolve(__dirname, 'src/guards'),
|
|
16
16
|
'@layouts': path.resolve(__dirname, 'src/layouts'),
|
|
17
|
+
'@channels': path.resolve(__dirname, 'src/channels'),
|
|
18
|
+
'@services': path.resolve(__dirname, 'src/services'),
|
|
17
19
|
},
|
|
18
20
|
},
|
|
19
21
|
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { html } from "lit";
|
|
2
|
-
import './okalit-router.js';
|
|
3
|
-
import { OkalitI18n } from './i18n.js';
|
|
4
|
-
|
|
5
|
-
export const AppMixin = (Base) => class extends Base {
|
|
6
|
-
async connectedCallback() {
|
|
7
|
-
super.connectedCallback();
|
|
8
|
-
if (this._appConfig.i18n && this._appConfig.i18n !== false) {
|
|
9
|
-
await OkalitI18n.init(this._appConfig.i18n);
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
get _getOkalitRouter() {
|
|
14
|
-
return html`
|
|
15
|
-
<okalit-router
|
|
16
|
-
.routes=${this._appConfig.routes}
|
|
17
|
-
.guards=${this._appConfig.guards}
|
|
18
|
-
.interceptors=${this._appConfig.interceptors}
|
|
19
|
-
></okalit-router>
|
|
20
|
-
`;
|
|
21
|
-
}
|
|
22
|
-
render() {
|
|
23
|
-
return html`
|
|
24
|
-
${ this._appConfig.layout
|
|
25
|
-
? html`${this._appConfig.layout(this._getOkalitRouter) }`
|
|
26
|
-
: this._getOkalitRouter }
|
|
27
|
-
`;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
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();
|
|
@@ -1,145 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|