@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.
Files changed (49) hide show
  1. package/README.md +13 -9
  2. package/lib/cli.js +38 -30
  3. package/package.json +1 -1
  4. package/templates/app/@okalit/Okalit.js +163 -89
  5. package/templates/app/@okalit/channel.js +177 -0
  6. package/templates/app/@okalit/define-element.js +30 -0
  7. package/templates/app/@okalit/i18n.js +88 -53
  8. package/templates/app/@okalit/index.js +8 -10
  9. package/templates/app/@okalit/mixins.js +106 -0
  10. package/templates/app/@okalit/performance.js +211 -0
  11. package/templates/app/@okalit/router-outlet.js +66 -0
  12. package/templates/app/@okalit/router.js +240 -0
  13. package/templates/app/@okalit/service.js +318 -23
  14. package/templates/app/index.html +0 -1
  15. package/templates/app/package.json +2 -3
  16. package/templates/app/public/i18n/en.json +47 -1
  17. package/templates/app/public/i18n/es.json +47 -1
  18. package/templates/app/src/{app.routes.ts → app.routes.js} +1 -1
  19. package/templates/app/src/channels/example.channel.js +15 -0
  20. package/templates/app/src/components/lazy-widget.js +13 -0
  21. package/templates/app/src/guards/auth.guard.js +17 -0
  22. package/templates/app/src/layouts/app-layout.js +75 -0
  23. package/templates/app/src/main-app.js +19 -9
  24. package/templates/app/src/modules/example/example.module.js +8 -3
  25. package/templates/app/src/modules/example/example.routes.js +27 -4
  26. package/templates/app/src/modules/example/pages/detail/example-detail.js +21 -0
  27. package/templates/app/src/modules/example/pages/home/example-home.js +39 -0
  28. package/templates/app/src/modules/example/pages/lifecycle/example-lifecycle.js +74 -0
  29. package/templates/app/src/modules/example/pages/performance/example-performance.js +59 -0
  30. package/templates/app/src/modules/example/pages/services/example-services.js +80 -0
  31. package/templates/app/src/services/rickandmorty.service.js +17 -0
  32. package/templates/app/src/services/user.service.js +33 -0
  33. package/templates/app/src/styles/global.scss +250 -0
  34. package/templates/app/src/styles/index.css +11 -2
  35. package/templates/app/vite.config.js +2 -0
  36. package/templates/app/@okalit/AppMixin.js +0 -29
  37. package/templates/app/@okalit/EventBus.js +0 -152
  38. package/templates/app/@okalit/ModuleMixin.js +0 -7
  39. package/templates/app/@okalit/OkalitService.js +0 -145
  40. package/templates/app/@okalit/defineElement.js +0 -65
  41. package/templates/app/@okalit/idle.js +0 -40
  42. package/templates/app/@okalit/lazy.js +0 -32
  43. package/templates/app/@okalit/okalit-router.js +0 -309
  44. package/templates/app/@okalit/trigger.js +0 -14
  45. package/templates/app/@okalit/viewport.js +0 -69
  46. package/templates/app/@okalit/when.js +0 -40
  47. package/templates/app/public/lit.svg +0 -1
  48. package/templates/app/src/modules/example/pages/example.page.js +0 -43
  49. 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
+ }
@@ -1,4 +1,13 @@
1
+ *, *::before, *::after { box-sizing: border-box; }
2
+
1
3
  body {
2
- margin: 0;
3
- padding: 0;
4
+ margin: 0;
5
+ padding: 0;
6
+ min-height: 100dvh;
7
+ background: #fafafa;
8
+ }
9
+
10
+ main-app {
11
+ display: block;
12
+ max-width: 100%;
4
13
  }
@@ -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,7 +0,0 @@
1
- import { html } from "lit";
2
-
3
- export const ModuleMixin = (Base) => class extends Base {
4
- render() {
5
- return html`<slot></slot>`;
6
- }
7
- }
@@ -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
- }