luxaura 1.0.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 +342 -0
- package/bin/luxaura.js +632 -0
- package/examples/TodoApp.lux +97 -0
- package/package.json +35 -0
- package/src/compiler/index.js +389 -0
- package/src/index.js +44 -0
- package/src/parser/index.js +319 -0
- package/src/runtime/luxaura.runtime.js +350 -0
- package/src/vault/server.js +207 -0
- package/templates/luxaura.config +43 -0
- package/ui-kit/luxaura.min.css +779 -0
- package/ui-kit/luxaura.min.js +271 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Luxaura Runtime Bundle v1.0.0
|
|
3
|
+
* Includes: Runtime Engine + Toast API + Router + Physics Engine
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* ── Inline Runtime ─────────────────────────────────────────────────────── */
|
|
7
|
+
(function (global) {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
let _uidCounter = 0;
|
|
11
|
+
const uid = () => ++_uidCounter;
|
|
12
|
+
|
|
13
|
+
function deepClone(obj) {
|
|
14
|
+
try { return JSON.parse(JSON.stringify(obj)); } catch { return obj; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function debounce(fn, ms) {
|
|
18
|
+
let t;
|
|
19
|
+
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getCsrfToken() {
|
|
23
|
+
let token = sessionStorage.getItem('__lux_csrf__');
|
|
24
|
+
if (!token) {
|
|
25
|
+
const buf = new Uint8Array(16);
|
|
26
|
+
crypto.getRandomValues(buf);
|
|
27
|
+
token = Array.from(buf).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
28
|
+
sessionStorage.setItem('__lux_csrf__', token);
|
|
29
|
+
}
|
|
30
|
+
return token;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Reactive State */
|
|
34
|
+
class ReactiveState {
|
|
35
|
+
constructor(initial, onChange) {
|
|
36
|
+
this._raw = deepClone(initial || {});
|
|
37
|
+
this._listeners = onChange ? [onChange] : [];
|
|
38
|
+
this._proxy = new Proxy(this._raw, {
|
|
39
|
+
set: (target, key, value) => {
|
|
40
|
+
target[key] = value;
|
|
41
|
+
this._notify();
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
get proxy() { return this._proxy; }
|
|
47
|
+
get(key) { return this._raw[key]; }
|
|
48
|
+
set(key, v) { this._raw[key] = v; this._notify(); }
|
|
49
|
+
_notify() { this._listeners.forEach(fn => fn(this._raw)); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* DOM Patcher */
|
|
53
|
+
function patch(container, newHTML) {
|
|
54
|
+
const tpl = document.createElement('template');
|
|
55
|
+
tpl.innerHTML = newHTML;
|
|
56
|
+
const newRoot = tpl.content;
|
|
57
|
+
diffNodes(container, newRoot);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function diffNodes(oldP, newP) {
|
|
61
|
+
const oldCh = Array.from(oldP.childNodes);
|
|
62
|
+
const newCh = Array.from(newP.childNodes);
|
|
63
|
+
const max = Math.max(oldCh.length, newCh.length);
|
|
64
|
+
for (let i = 0; i < max; i++) {
|
|
65
|
+
const o = oldCh[i], n = newCh[i];
|
|
66
|
+
if (!o && n) { oldP.appendChild(n.cloneNode(true)); continue; }
|
|
67
|
+
if (o && !n) { oldP.removeChild(o); continue; }
|
|
68
|
+
if (o.nodeType !== n.nodeType || o.nodeName !== n.nodeName) {
|
|
69
|
+
oldP.replaceChild(n.cloneNode(true), o); continue;
|
|
70
|
+
}
|
|
71
|
+
if (o.nodeType === Node.TEXT_NODE) {
|
|
72
|
+
if (o.textContent !== n.textContent) o.textContent = n.textContent;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (o.nodeType === Node.ELEMENT_NODE) {
|
|
76
|
+
diffAttrs(o, n);
|
|
77
|
+
diffNodes(o, n);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function diffAttrs(o, n) {
|
|
83
|
+
for (const a of Array.from(o.attributes))
|
|
84
|
+
if (!n.hasAttribute(a.name)) o.removeAttribute(a.name);
|
|
85
|
+
for (const a of Array.from(n.attributes))
|
|
86
|
+
if (o.getAttribute(a.name) !== a.value) o.setAttribute(a.name, a.value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* RPC */
|
|
90
|
+
const rpc = new Proxy({}, {
|
|
91
|
+
get(_, fn) {
|
|
92
|
+
return async (...args) => {
|
|
93
|
+
const r = await fetch('/__lux_rpc__', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json', 'X-Lux-CSRF': getCsrfToken() },
|
|
96
|
+
body: JSON.stringify({ fn, args }),
|
|
97
|
+
});
|
|
98
|
+
if (!r.ok) throw new Error(`RPC ${fn} failed: ${r.status}`);
|
|
99
|
+
return r.json();
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/* Router */
|
|
105
|
+
const router = {
|
|
106
|
+
params: {}, query: {},
|
|
107
|
+
navigate(url) { history.pushState({}, '', url); window.dispatchEvent(new PopStateEvent('popstate')); },
|
|
108
|
+
_parseQuery(s) { const q = {}; new URLSearchParams(s).forEach((v,k) => q[k]=v); return q; },
|
|
109
|
+
_init() {
|
|
110
|
+
this.query = this._parseQuery(location.search);
|
|
111
|
+
window.addEventListener('popstate', () => { this.query = this._parseQuery(location.search); });
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* Storage */
|
|
116
|
+
const storage = {
|
|
117
|
+
local: { get: k => localStorage.getItem(k), set: (k,v) => localStorage.setItem(k,v), remove: k => localStorage.removeItem(k) },
|
|
118
|
+
session: { get: k => sessionStorage.getItem(k), set: (k,v) => sessionStorage.setItem(k,v), remove: k => sessionStorage.removeItem(k) },
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/* HTTP */
|
|
122
|
+
const http = {
|
|
123
|
+
async get(url, o={}) { return (await fetch(url, {method:'GET',...o})).json(); },
|
|
124
|
+
async post(url, body, o={}) {
|
|
125
|
+
return (await fetch(url, { method:'POST', headers:{'Content-Type':'application/json',...(o.headers||{})}, body:JSON.stringify(body), ...o })).json();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const auth = {
|
|
130
|
+
async login(c) { return rpc.login(c); },
|
|
131
|
+
async logout() { return rpc.logout(); },
|
|
132
|
+
async user() { return rpc.currentUser(); },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/* Physics Engine */
|
|
136
|
+
const physics = {
|
|
137
|
+
_init() {
|
|
138
|
+
this._apply();
|
|
139
|
+
window.addEventListener('resize', debounce(() => this._apply(), 100));
|
|
140
|
+
},
|
|
141
|
+
_apply() {
|
|
142
|
+
const mobile = window.innerWidth < 768;
|
|
143
|
+
document.querySelectorAll('.lux-action, .lux-input, .lux-switch').forEach(el => {
|
|
144
|
+
el.style.minHeight = mobile ? '44px' : '';
|
|
145
|
+
el.style.minWidth = mobile ? '44px' : '';
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/* Toast API */
|
|
151
|
+
let toastContainer;
|
|
152
|
+
function ensureToastContainer() {
|
|
153
|
+
if (!toastContainer) {
|
|
154
|
+
toastContainer = document.createElement('div');
|
|
155
|
+
toastContainer.className = 'lux-toast-container';
|
|
156
|
+
document.body.appendChild(toastContainer);
|
|
157
|
+
}
|
|
158
|
+
return toastContainer;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const toast = {
|
|
162
|
+
_show(msg, type, duration = 3500) {
|
|
163
|
+
const el = document.createElement('div');
|
|
164
|
+
el.className = `lux-toast ${type}`;
|
|
165
|
+
el.textContent = msg;
|
|
166
|
+
const container = ensureToastContainer();
|
|
167
|
+
container.appendChild(el);
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
el.style.opacity = '0';
|
|
170
|
+
el.style.transform = 'translateX(20px)';
|
|
171
|
+
el.style.transition = 'opacity 0.3s, transform 0.3s';
|
|
172
|
+
setTimeout(() => el.remove(), 300);
|
|
173
|
+
}, duration);
|
|
174
|
+
return el;
|
|
175
|
+
},
|
|
176
|
+
success(msg, d) { return this._show(msg, 'success', d); },
|
|
177
|
+
error(msg, d) { return this._show(msg, 'error', d); },
|
|
178
|
+
warning(msg, d) { return this._show(msg, 'warning', d); },
|
|
179
|
+
info(msg, d) { return this._show(msg, 'info', d); },
|
|
180
|
+
show(msg, d) { return this._show(msg, '', d); },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/* Component Context */
|
|
184
|
+
class ComponentContext {
|
|
185
|
+
constructor(def, props = {}) {
|
|
186
|
+
this._uid = uid();
|
|
187
|
+
this._def = def;
|
|
188
|
+
this._props = props;
|
|
189
|
+
this._state = new ReactiveState(deepClone(def.state || {}), () => this._scheduleUpdate());
|
|
190
|
+
this._el = null;
|
|
191
|
+
this._pending = false;
|
|
192
|
+
this.server = rpc;
|
|
193
|
+
this.router = router;
|
|
194
|
+
this.http = http;
|
|
195
|
+
this.auth = auth;
|
|
196
|
+
this.storage = storage;
|
|
197
|
+
this.toast = toast;
|
|
198
|
+
}
|
|
199
|
+
get(k) { return k in this._props ? this._props[k] : this._state.get(k); }
|
|
200
|
+
set(k, v) { this._state.set(k, v); }
|
|
201
|
+
_scheduleUpdate() {
|
|
202
|
+
if (this._pending) return;
|
|
203
|
+
this._pending = true;
|
|
204
|
+
requestAnimationFrame(() => { this._pending = false; this._update(); });
|
|
205
|
+
}
|
|
206
|
+
_update() {
|
|
207
|
+
if (!this._el) return;
|
|
208
|
+
patch(this._el, this._def.render(this));
|
|
209
|
+
}
|
|
210
|
+
mount(sel) {
|
|
211
|
+
const el = typeof sel === 'string' ? document.querySelector(sel) : sel;
|
|
212
|
+
if (!el) throw new Error(`Luxaura: mount target not found: ${sel}`);
|
|
213
|
+
this._el = el;
|
|
214
|
+
el.innerHTML = this._def.render(this);
|
|
215
|
+
el.__luxCtx__ = this;
|
|
216
|
+
if (this._def.mounted) this._def.mounted(el, this);
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Registry */
|
|
222
|
+
const registry = {};
|
|
223
|
+
|
|
224
|
+
const Lux = {
|
|
225
|
+
version: '1.0.0',
|
|
226
|
+
define(name, def) { registry[name] = def; return this; },
|
|
227
|
+
mount(name, sel, props={}) {
|
|
228
|
+
const def = registry[name];
|
|
229
|
+
if (!def) throw new Error(`Luxaura: component "${name}" not defined.`);
|
|
230
|
+
return new ComponentContext(def, props).mount(sel);
|
|
231
|
+
},
|
|
232
|
+
on(container, component, event, handler) {
|
|
233
|
+
const sel = `[data-lux-id^="${component.toLowerCase()}-"]`;
|
|
234
|
+
container.addEventListener(event, async (e) => {
|
|
235
|
+
const t = e.target.closest(sel);
|
|
236
|
+
if (!t) return;
|
|
237
|
+
await handler(t.__luxCtx__ || container.__luxCtx__ || {});
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
bind(container, ctx) {
|
|
241
|
+
container.addEventListener('input', e => {
|
|
242
|
+
const k = e.target.dataset.luxBind;
|
|
243
|
+
if (k) ctx.set(k, e.target.value);
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
toast, router, http, auth, storage, rpc,
|
|
247
|
+
_init() { router._init(); physics._init(); },
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/* Theme helper */
|
|
251
|
+
Lux.setTheme = function(t) {
|
|
252
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
253
|
+
storage.local.set('lux-theme', t);
|
|
254
|
+
};
|
|
255
|
+
Lux.getTheme = function() {
|
|
256
|
+
return storage.local.get('lux-theme') || 'light';
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/* Auto-apply saved theme */
|
|
260
|
+
const savedTheme = (localStorage.getItem('lux-theme'));
|
|
261
|
+
if (savedTheme) document.documentElement.setAttribute('data-theme', savedTheme);
|
|
262
|
+
|
|
263
|
+
/* Boot */
|
|
264
|
+
if (document.readyState === 'loading') {
|
|
265
|
+
document.addEventListener('DOMContentLoaded', () => Lux._init());
|
|
266
|
+
} else {
|
|
267
|
+
Lux._init();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
global.__Lux__ = Lux;
|
|
271
|
+
})(window);
|