@firesmasher/velox.js 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.
@@ -0,0 +1,1400 @@
1
+ export const templates = {
2
+
3
+ config: (a) => `// Velox.js Configuration File
4
+ // Project: ${a.projectName}
5
+ // Important — configure paths precisely before use.
6
+ {
7
+ // Core paths
8
+ "tagsFolder": "../../tags",
9
+ "pagesFolder": "../../public",
10
+
11
+ // URL registry
12
+ "localUrlJson": "../../tags/json/UrlS.json",
13
+
14
+ // Variable storage
15
+ "variableStorage": "../../storage/variables/normal.json",
16
+ "SHA1VariableStorage": "../../storage/variables/sha1.json",
17
+
18
+ // Assets
19
+ "mainCssLocation": "../../assets/css/style.css",
20
+ "savedFilesPath": "../../tags/savedFiles",
21
+
22
+ // Icons (not automatically created — add your own files here)
23
+ "icons": {
24
+ "icon.png": "../../assets/icons/png/icon.png",
25
+ "icon.svg": "../../assets/icons/svg/icon.svg"
26
+ },
27
+
28
+ // Quick path imports — lets you use @env:key in .vlx files
29
+ "QuickPathsVariableStorage": "../../storage/env/quickPaths.json"
30
+ }
31
+ `,
32
+
33
+ quickPaths: () => `{}
34
+ `,
35
+
36
+ urlS: () => `{}
37
+ `,
38
+
39
+ css: (a) => `/* Velox.js — Main Stylesheet */
40
+ /* Project: ${a.projectName} */
41
+
42
+ *, *::before, *::after {
43
+ box-sizing: border-box;
44
+ margin: 0;
45
+ padding: 0;
46
+ }
47
+
48
+ :root {
49
+ --velox-bg: #0d0d0d;
50
+ --velox-surface: #161616;
51
+ --velox-accent: #00c2ff;
52
+ --velox-text: #e8e8e8;
53
+ --velox-muted: #6b6b6b;
54
+ --velox-font: system-ui, -apple-system, sans-serif;
55
+ }
56
+
57
+ body {
58
+ background: var(--velox-bg);
59
+ color: var(--velox-text);
60
+ font-family: var(--velox-font);
61
+ line-height: 1.6;
62
+ padding: 2rem;
63
+ }
64
+
65
+ button {
66
+ background: var(--velox-accent);
67
+ color: #000;
68
+ border: none;
69
+ padding: .5rem 1.2rem;
70
+ border-radius: 6px;
71
+ cursor: pointer;
72
+ font-size: 1rem;
73
+ }
74
+
75
+ button:hover { opacity: .85; }
76
+ `,
77
+
78
+ // ── How to connect a .vlx to HTML ──────────────────────────────────────────
79
+ //
80
+ // Option 1 — inline tag (runs at page load):
81
+ // <?velox Dom.setText("Hello") $target="$id(myDiv)" >
82
+ //
83
+ // Option 2 — run a .vlx file at page load:
84
+ // <?velox Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(out)" >
85
+ //
86
+ // Option 3 — event listener (runs on user interaction):
87
+ // <?velox $on click "$id(myBtn)": Dom.setText("Clicked!") $target="$id(out)" >
88
+ // <?velox $on click "$id(myBtn)": Velox.run("myscript.vlx") $target="$id(out)" >
89
+ //
90
+ // The runtime auto-scans for all <?velox ...> tags on DOMContentLoaded.
91
+ // velox.js MUST be loaded BEFORE the closing </body> tag.
92
+
93
+ indexHtml: (a) => `<!DOCTYPE html>
94
+ <html lang="en">
95
+ <head>
96
+ <meta charset="UTF-8" />
97
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
98
+ <title>${a.projectName}</title>
99
+ <link rel="stylesheet" href="../assets/css/style.css" />
100
+ </head>
101
+ <body>
102
+
103
+ <h1>Welcome to ${a.projectName}</h1>
104
+ <p>Your Velox.js project is ready.</p>
105
+
106
+ <!-- Output element -->
107
+ <div id="output" style="margin-top:1rem;"></div>
108
+
109
+ <!-- Button wired by a <?velox $on > tag below -->
110
+ <button id="myBtn" style="margin-top:1rem;">Click me</button>
111
+
112
+ <!--
113
+ <?velox > tags are scanned at page load by the runtime.
114
+
115
+ Example 1 — run inline velox on page load:
116
+ <?velox Dom.setText("Page loaded!") $target="$id(output)" >
117
+
118
+ Example 2 — run a .vlx file on page load:
119
+ <?velox Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(output)" >
120
+
121
+ Example 3 — wire a click event to a .vlx file:
122
+ <?velox $on click "$id(myBtn)": Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(output)" >
123
+
124
+ Example 4 — inline command on click:
125
+ <?velox $on click "$id(myBtn)": Dom.setText("Hello from Velox!") $target="$id(output)" >
126
+ -->
127
+
128
+ <!-- Active example: click button → run inline velox command -->
129
+ <?velox $on click "$id(myBtn)": Dom.setText("Hello from Velox!") $target="$id(output)" >
130
+
131
+ <!-- Velox.js runtime — must come BEFORE closing </body> -->
132
+ <script src="../assets/scripts/velox/velox.js"></script>
133
+
134
+ </body>
135
+ </html>
136
+ `,
137
+
138
+ projectMeta: (a) => JSON.stringify({
139
+ name: a.projectName,
140
+ version: a.version || "1.0.0",
141
+ author: a.author || "",
142
+ framework: "velox.js",
143
+ frameworkVersion: "2.0.0",
144
+ created: new Date().toISOString().split("T")[0],
145
+ }, null, 2) + "\n",
146
+
147
+ gitignore: () => `# Velox.js
148
+ node_modules/
149
+ .DS_Store
150
+ Thumbs.db
151
+ *.log
152
+
153
+ # Local secrets / overrides
154
+ storage/variables/sha1.json
155
+ `,
156
+
157
+ // myscript.vlx — starter script
158
+ starterVlx: () => `// myscript.vlx — Velox.js starter script
159
+ // Statements are separated by ;
160
+ // Run this from HTML with:
161
+ // <?velox Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(output)" >
162
+
163
+ Dom.setText("Script ran successfully!")
164
+ $if .: echo("Target updated")
165
+ $else: echo.err("No target found");
166
+
167
+ echo("myscript.vlx loaded");
168
+ `,
169
+
170
+ veloxRuntime: () => `/**
171
+ * Velox.js Runtime v2.0.0
172
+ *
173
+ * ── How to connect a .vlx to HTML ──────────────────────────────────────────
174
+ *
175
+ * 1. Inline tag (runs at page load):
176
+ * <?velox Dom.setText("Hello World") $target="$id(myDiv)" >
177
+ *
178
+ * 2. Run a .vlx file:
179
+ * <?velox Velox.run("myscript.vlx") $target="$id(myDiv)" >
180
+ *
181
+ * 3. Event-driven (runs on click, hover, etc.):
182
+ * <?velox $on click "$id(btn)": Velox.run("script.vlx") $target="$id(out)" >
183
+ * <?velox $on click "$id(btn)": Dom.setText("Clicked!") $target="$id(out)" >
184
+ *
185
+ * ── Value rules ─────────────────────────────────────────────────────────────
186
+ * 0 = false / none
187
+ * 1 = true
188
+ * . = pass / blank / zero
189
+ * _ = same as .
190
+ *
191
+ * ── Statement separator ──────────────────────────────────────────────────────
192
+ * ; ends a statement (use inside .vlx files and inline tags)
193
+ *
194
+ * ── Target selectors ────────────────────────────────────────────────────────
195
+ * $id(myId) → document.getElementById("myId")
196
+ * $class(myClass) → first element with that class
197
+ * $query(selector) → document.querySelector(any CSS selector)
198
+ * $tag(div) → first element of that tag
199
+ *
200
+ * ── Commands reference ───────────────────────────────────────────────────────
201
+ *
202
+ * DOM
203
+ * Dom.setText("text") set textContent
204
+ * Dom.setHtml("html") set innerHTML
205
+ * Dom.append("html") appendChild parsed HTML
206
+ * Dom.prepend("html") prependChild parsed HTML
207
+ * Dom.remove() remove element from DOM
208
+ * Dom.clear() clear innerHTML
209
+ * Dom.show() remove display:none
210
+ * Dom.hide() display:none
211
+ * Dom.toggle() toggle visibility
212
+ * Dom.addClass("cls") add class
213
+ * Dom.removeClass("cls") remove class
214
+ * Dom.toggleClass("cls") toggle class
215
+ * Dom.setAttr("attr:value") set attribute
216
+ * Dom.removeAttr("attr") remove attribute
217
+ * Dom.setStyle("prop:value") set inline style
218
+ * Dom.focus() focus element
219
+ * Dom.click() programmatic click
220
+ * Dom.scrollTo() scroll element into view
221
+ * Dom.getValue() log input value to console
222
+ * Dom.setValue("val") set input value
223
+ *
224
+ * ANIMATION
225
+ * Anim.fadeIn("duration") fade in (ms)
226
+ * Anim.fadeOut("duration") fade out (ms)
227
+ * Anim.slide("down|up:duration") slide open or close (ms)
228
+ * Anim.shake() shake element
229
+ * Anim.pulse("duration") pulse element
230
+ *
231
+ * EVENTS
232
+ * $on click "$id(btn)": command() attach click listener
233
+ * $on hover "$id(btn)": command() attach mouseenter listener
234
+ * $on input "$id(inp)": command() attach input listener
235
+ * $on change "$id(sel)": command() attach change listener
236
+ * $on submit "$id(frm)": command() attach submit listener (prevents default)
237
+ * $on keydown "$id(el)": command() attach keydown listener
238
+ * $on load ".": command() window load listener
239
+ *
240
+ * FORM
241
+ * Form.getValue("$id(inp)") log value of input
242
+ * Form.setValue("$id(inp):val") set value of input
243
+ * Form.clear("$id(frm)") clear all inputs in a form
244
+ * Form.serialize("$id(frm)") log form data as JSON
245
+ *
246
+ * LOCAL STORAGE
247
+ * Store.set("key:value") localStorage.setItem
248
+ * Store.get("key") localStorage.getItem (logs result)
249
+ * Store.remove("key") localStorage.removeItem
250
+ * Store.clear() localStorage.clear
251
+ *
252
+ * COOKIE
253
+ * Cookie.set("key:value") document.cookie setter
254
+ * Cookie.get("key") cookie getter (logs result)
255
+ * Cookie.remove("key") expire cookie
256
+ *
257
+ * VARIABLES
258
+ * Var.set("key:value") set in-memory variable
259
+ * Var.get("key") log in-memory variable
260
+ * Var.clear("key|*") clear one or all variables
261
+ *
262
+ * NETWORK
263
+ * Url.get("url") fetch text, insert into target
264
+ * Url.getJson("url") fetch JSON, insert as pretty text
265
+ * Url.post("url") POST request
266
+ * Url.postJson("url:body") POST with JSON body
267
+ * Url.fetch("url") generic fetch (download if $type="download")
268
+ * iframe.Url("url") embed URL in iframe inside target
269
+ *
270
+ * FILE
271
+ * file.localFetch("path") fetch local file (download if $type="download")
272
+ *
273
+ * PAGE
274
+ * Page.redirect("url") navigate to URL
275
+ * Page.reload() reload current page
276
+ * Page.title("text") set document title
277
+ * Page.meta("name:content") set/update meta tag
278
+ * Page.scrollTop() scroll window to top
279
+ *
280
+ * ROUTER (SPA)
281
+ * Router.define("route:vlxPath") register a SPA route
282
+ * Router.go("route") navigate to a route
283
+ * Router.start("$id(outlet)") start router with hash-based routing
284
+ *
285
+ * TEMPLATE
286
+ * Template.render("tpl:data") render a <template id="tpl"> with JSON data
287
+ *
288
+ * SCRIPT / STYLE
289
+ * Script.load("url") load external JS file
290
+ * Style.load("url") load external CSS file
291
+ *
292
+ * LOGGING
293
+ * echo("msg") console.log
294
+ * echo.warn("msg") console.warn
295
+ * echo.err("msg") console.error
296
+ */
297
+
298
+ const Velox = (() => {
299
+
300
+ let _config = null;
301
+ let _quickPaths = {};
302
+ let _vars = {};
303
+ const _routes = {};
304
+ let _routerOutlet = null;
305
+
306
+ // ── Logging ────────────────────────────────────────────────────────────────
307
+
308
+ const log = (msg) => console.log(\`[Velox] \${msg}\`);
309
+ const warn = (msg) => console.warn(\`[Velox] \${msg}\`);
310
+ const err = (msg) => console.error(\`[Velox] \${msg}\`);
311
+
312
+ // ── Config ─────────────────────────────────────────────────────────────────
313
+
314
+ async function loadConfig() {
315
+ try {
316
+ const res = await fetch("/.velox/conf/config.jsonc");
317
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
318
+ const text = await res.text();
319
+ const stripped = text.replace(/\\/\\/.*$/gm, "").replace(/,(\\s*[}\\]])/g, "$1");
320
+ _config = JSON.parse(stripped);
321
+ } catch (e) {
322
+ _config = {};
323
+ }
324
+ }
325
+
326
+ async function loadQuickPaths() {
327
+ try {
328
+ const p = _config?.QuickPathsVariableStorage || "/storage/env/quickPaths.json";
329
+ const res = await fetch(resolveRelative(p));
330
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
331
+ _quickPaths = await res.json();
332
+ } catch (e) {
333
+ _quickPaths = {};
334
+ }
335
+ }
336
+
337
+ async function loadVars() {
338
+ try {
339
+ const p = _config?.variableStorage || "/storage/variables/normal.json";
340
+ const res = await fetch(resolveRelative(p));
341
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
342
+ _vars = await res.json();
343
+ } catch (e) {
344
+ _vars = {};
345
+ }
346
+ }
347
+
348
+ // ── Path helpers ───────────────────────────────────────────────────────────
349
+
350
+ function resolveEnv(value) {
351
+ return value.replace(/@env:([a-zA-Z0-9_]+)/g, (_, key) => {
352
+ if (_quickPaths[key] !== undefined) return _quickPaths[key];
353
+ warn(\`@env:\${key} not found in quickPaths.json\`);
354
+ return value;
355
+ });
356
+ }
357
+
358
+ function resolveRelative(p) {
359
+ if (!p) return p;
360
+ if (p.startsWith("http://") || p.startsWith("https://")) return p;
361
+ if (p.startsWith("/")) return p;
362
+ // Convert relative paths (../../) to absolute from root
363
+ return p.replace(/^(\\.\\.\\/)+/, "/").replace(/^\\.\\//, "/");
364
+ }
365
+
366
+ function resolvePath(raw) {
367
+ return resolveRelative(resolveEnv(raw.trim()));
368
+ }
369
+
370
+ // ── Selector resolver ──────────────────────────────────────────────────────
371
+ // Supports: $id(x) $class(x) $query(x) $tag(x)
372
+ // Also accepts raw CSS selectors directly
373
+
374
+ function resolveTarget(target) {
375
+ if (!target || target === "." || target === "_") return document.body;
376
+ if (target instanceof Element) return target;
377
+ if (typeof target !== "string") return document.body;
378
+
379
+ const t = target.trim();
380
+ const idMatch = t.match(/^\\$id\\(([^)]+)\\)$/);
381
+ const classMatch = t.match(/^\\$class\\(([^)]+)\\)$/);
382
+ const queryMatch = t.match(/^\\$query\\((.+)\\)$/);
383
+ const tagMatch = t.match(/^\\$tag\\(([^)]+)\\)$/);
384
+
385
+ if (idMatch) return document.getElementById(idMatch[1]);
386
+ if (classMatch) return document.getElementsByClassName(classMatch[1])[0] || null;
387
+ if (queryMatch) return document.querySelector(queryMatch[1]);
388
+ if (tagMatch) return document.getElementsByTagName(tagMatch[1])[0] || null;
389
+
390
+ // Last resort: treat as a CSS selector
391
+ return document.querySelector(t) || document.body;
392
+ }
393
+
394
+ // ── Value rules ────────────────────────────────────────────────────────────
395
+
396
+ function resolveValue(v) {
397
+ if (v === "1") return true;
398
+ if (v === "0") return false;
399
+ if (v === "." || v === "_") return null;
400
+ return v;
401
+ }
402
+
403
+ // ── Block / modifier parser ────────────────────────────────────────────────
404
+ // Matches: $keyword [value]: callStr
405
+ // e.g. $if true: echo("ok")
406
+ // $on click "$id(btn)": Dom.show()
407
+
408
+ function parseBlocks(src) {
409
+ const blocks = [];
410
+ // Extended to support $on events
411
+ const blockRe = /\\$(if else|if|else|on|set|delay|repeat|log|err)\\s*([^:]*)?:\\s*([^;>]+)/g;
412
+ let m;
413
+ while ((m = blockRe.exec(src)) !== null) {
414
+ blocks.push({
415
+ keyword: m[1].trim(),
416
+ value: m[2] ? m[2].trim() : null,
417
+ call: m[3].trim(),
418
+ });
419
+ }
420
+ return blocks;
421
+ }
422
+
423
+ // ── .vlx source tokenizer ──────────────────────────────────────────────────
424
+ // Splits on ; but respects quoted strings and nested parens
425
+
426
+ function tokenizeVlx(source) {
427
+ // Strip line comments
428
+ source = source.replace(/\\/\\/.*$/gm, "");
429
+ const stmts = [];
430
+ let buf = "";
431
+ let depth = 0;
432
+ let inStr = false;
433
+ let strChar = "";
434
+
435
+ for (let i = 0; i < source.length; i++) {
436
+ const c = source[i];
437
+ if (inStr) {
438
+ buf += c;
439
+ if (c === strChar && source[i - 1] !== "\\\\") inStr = false;
440
+ } else if (c === '"' || c === "'") {
441
+ inStr = true; strChar = c; buf += c;
442
+ } else if (c === "(") {
443
+ depth++; buf += c;
444
+ } else if (c === ")") {
445
+ depth--; buf += c;
446
+ } else if (c === ";" && depth === 0) {
447
+ const trimmed = buf.trim();
448
+ if (trimmed) stmts.push(trimmed);
449
+ buf = "";
450
+ } else {
451
+ buf += c;
452
+ }
453
+ }
454
+ const trimmed = buf.trim();
455
+ if (trimmed) stmts.push(trimmed);
456
+ return stmts;
457
+ }
458
+
459
+ // ── Statement parser ────────────────────────────────────────────────────────
460
+ // Parses one statement: Cmd.name("arg") [$modifiers]
461
+ // Also handles $on event blocks
462
+
463
+ function parseStatement(stmt) {
464
+ stmt = stmt.trim();
465
+ if (!stmt) return null;
466
+
467
+ // Check for $on event block: $on click "$id(btn)": Dom.show()
468
+ const onMatch = stmt.match(/^\\$on\\s+(\\w+)\\s+"([^"]+)":\\s*(.+)$/s);
469
+ if (onMatch) {
470
+ return {
471
+ fn: "$on",
472
+ event: onMatch[1].trim(),
473
+ selector: onMatch[2].trim(),
474
+ call: onMatch[3].trim(),
475
+ delay: 0,
476
+ repeat: 1,
477
+ blocks: [],
478
+ };
479
+ }
480
+
481
+ // Standard call: Fn.name("arg") [$mods]
482
+ // arg may contain any chars except unescaped "
483
+ const callMatch = stmt.match(/^([a-zA-Z$][a-zA-Z0-9$.]*)\\("((?:[^"\\\\]|\\\\.)*)"\\)([\\s\\S]*)$/);
484
+ if (callMatch) {
485
+ const fn = callMatch[1];
486
+ const arg = callMatch[2].replace(/\\\\"/g, '"');
487
+ const rest = callMatch[3];
488
+
489
+ const typeMatch = rest.match(/\\$type\\s*=\\s*"([^"]+)"/);
490
+ const targetMatch = rest.match(/\\$target\\s*=\\s*"([^"]+)"/);
491
+ const delayMatch = rest.match(/\\$delay\\s+(\\d+)/);
492
+ const repeatMatch = rest.match(/\\$repeat\\s+(\\d+)/);
493
+
494
+ return {
495
+ fn,
496
+ arg,
497
+ type: typeMatch ? typeMatch[1] : null,
498
+ target: targetMatch ? targetMatch[1] : null,
499
+ delay: delayMatch ? parseInt(delayMatch[1]) : 0,
500
+ repeat: repeatMatch ? parseInt(repeatMatch[1]) : 1,
501
+ blocks: parseBlocks(rest),
502
+ };
503
+ }
504
+
505
+ // Zero-arg call: Dom.remove() Dom.show() etc.
506
+ const noArgMatch = stmt.match(/^([a-zA-Z$][a-zA-Z0-9$.]*)\\(\\)([\\s\\S]*)$/);
507
+ if (noArgMatch) {
508
+ const fn = noArgMatch[1];
509
+ const rest = noArgMatch[2];
510
+ const typeMatch = rest.match(/\\$type\\s*=\\s*"([^"]+)"/);
511
+ const targetMatch = rest.match(/\\$target\\s*=\\s*"([^"]+)"/);
512
+ const delayMatch = rest.match(/\\$delay\\s+(\\d+)/);
513
+ const repeatMatch = rest.match(/\\$repeat\\s+(\\d+)/);
514
+ return {
515
+ fn,
516
+ arg: "",
517
+ type: typeMatch ? typeMatch[1] : null,
518
+ target: targetMatch ? targetMatch[1] : null,
519
+ delay: delayMatch ? parseInt(delayMatch[1]) : 0,
520
+ repeat: repeatMatch ? parseInt(repeatMatch[1]) : 1,
521
+ blocks: parseBlocks(rest),
522
+ };
523
+ }
524
+
525
+ warn(\`Could not parse statement: \${stmt}\`);
526
+ return null;
527
+ }
528
+
529
+ function parseVlx(source) {
530
+ return tokenizeVlx(source)
531
+ .map(parseStatement)
532
+ .filter(Boolean);
533
+ }
534
+
535
+ // ── <?velox ...> tag scanner ───────────────────────────────────────────────
536
+
537
+ function extractVeloxTags(html) {
538
+ const results = [];
539
+ // Match <?velox ... > — non-greedy, supports multiline
540
+ const re = /<\\?velox([\\s\\S]*?)>/g;
541
+ let m;
542
+ while ((m = re.exec(html)) !== null) {
543
+ results.push(m[1].trim());
544
+ }
545
+ return results;
546
+ }
547
+
548
+ // ── Inline tag parser: handles Velox.run("path") $target="..." ────────────
549
+
550
+ function parseInlineTag(src) {
551
+ // Velox.run("path") [$target="..."]
552
+ const runMatch = src.match(/^Velox\\.run\\("([^"]+)"\\)([\\s\\S]*)$/);
553
+ if (runMatch) {
554
+ const vlxPath = runMatch[1];
555
+ const rest = runMatch[2];
556
+ const targetMatch = rest.match(/\\$target\\s*=\\s*"([^"]+)"/);
557
+ const typeMatch = rest.match(/\\$type\\s*=\\s*"([^"]+)"/);
558
+ return {
559
+ kind: "run",
560
+ vlxPath,
561
+ target: targetMatch ? targetMatch[1] : null,
562
+ type: typeMatch ? typeMatch[1] : null,
563
+ blocks: parseBlocks(rest),
564
+ };
565
+ }
566
+ return null;
567
+ }
568
+
569
+ // ── Animation helpers ──────────────────────────────────────────────────────
570
+
571
+ function animFadeIn(el, ms = 300) {
572
+ el.style.transition = \`opacity \${ms}ms\`;
573
+ el.style.opacity = "0";
574
+ el.style.display = "";
575
+ requestAnimationFrame(() => {
576
+ requestAnimationFrame(() => { el.style.opacity = "1"; });
577
+ });
578
+ }
579
+
580
+ function animFadeOut(el, ms = 300) {
581
+ el.style.transition = \`opacity \${ms}ms\`;
582
+ el.style.opacity = "0";
583
+ setTimeout(() => { el.style.display = "none"; }, ms);
584
+ }
585
+
586
+ function animSlide(el, dir, ms = 300) {
587
+ if (dir === "down") {
588
+ el.style.overflow = "hidden";
589
+ el.style.height = "0";
590
+ el.style.display = "";
591
+ const h = el.scrollHeight;
592
+ el.style.transition = \`height \${ms}ms\`;
593
+ requestAnimationFrame(() => {
594
+ requestAnimationFrame(() => { el.style.height = h + "px"; });
595
+ });
596
+ setTimeout(() => { el.style.height = ""; el.style.overflow = ""; }, ms);
597
+ } else {
598
+ el.style.overflow = "hidden";
599
+ el.style.height = el.scrollHeight + "px";
600
+ el.style.transition = \`height \${ms}ms\`;
601
+ requestAnimationFrame(() => {
602
+ requestAnimationFrame(() => { el.style.height = "0"; });
603
+ });
604
+ setTimeout(() => { el.style.display = "none"; el.style.height = ""; el.style.overflow = ""; }, ms);
605
+ }
606
+ }
607
+
608
+ function animShake(el) {
609
+ el.style.animation = "none";
610
+ const id = \`vlx-shake-\${Date.now()}\`;
611
+ if (!document.getElementById(id)) {
612
+ const s = document.createElement("style");
613
+ s.id = id;
614
+ s.textContent = \`@keyframes vlx-shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-8px)}40%,80%{transform:translateX(8px)}}\`;
615
+ document.head.appendChild(s);
616
+ }
617
+ el.style.animation = "vlx-shake 0.4s";
618
+ setTimeout(() => { el.style.animation = ""; }, 400);
619
+ }
620
+
621
+ function animPulse(el, ms = 600) {
622
+ const id = \`vlx-pulse-\${Date.now()}\`;
623
+ if (!document.getElementById(id)) {
624
+ const s = document.createElement("style");
625
+ s.id = id;
626
+ s.textContent = \`@keyframes vlx-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}\`;
627
+ document.head.appendChild(s);
628
+ }
629
+ el.style.animation = \`vlx-pulse \${ms}ms\`;
630
+ setTimeout(() => { el.style.animation = ""; }, ms);
631
+ }
632
+
633
+ // ── Cookie helpers ─────────────────────────────────────────────────────────
634
+
635
+ function cookieGet(key) {
636
+ const m = document.cookie.match(new RegExp(\`(?:^|;\\\\s*)\${encodeURIComponent(key)}=([^;]*)\`));
637
+ return m ? decodeURIComponent(m[1]) : null;
638
+ }
639
+
640
+ function cookieSet(key, value, days = 365) {
641
+ const exp = new Date(Date.now() + days * 864e5).toUTCString();
642
+ document.cookie = \`\${encodeURIComponent(key)}=\${encodeURIComponent(value)};expires=\${exp};path=/\`;
643
+ }
644
+
645
+ function cookieRemove(key) {
646
+ document.cookie = \`\${encodeURIComponent(key)}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/\`;
647
+ }
648
+
649
+ // ── Template rendering ─────────────────────────────────────────────────────
650
+ // <template id="myTpl"><div>{{name}}</div></template>
651
+ // Template.render("myTpl:{\\"name\\":\\"World\\"}")
652
+
653
+ function renderTemplate(tplId, data, target) {
654
+ const tpl = document.getElementById(tplId);
655
+ if (!tpl || tpl.tagName !== "TEMPLATE") {
656
+ warn(\`Template #\${tplId} not found\`);
657
+ return false;
658
+ }
659
+ let html = tpl.innerHTML;
660
+ for (const [k, v] of Object.entries(data)) {
661
+ html = html.replaceAll(\`{{\${k}}}\`, v);
662
+ }
663
+ if (target) target.innerHTML = html;
664
+ return true;
665
+ }
666
+
667
+ // ── SPA Router ────────────────────────────────────────────────────────────
668
+
669
+ async function routerNavigate(route) {
670
+ const vlxPath = _routes[route] || _routes["*"];
671
+ if (!vlxPath) { warn(\`No route defined for: \${route}\`); return; }
672
+ window.location.hash = route;
673
+ if (_routerOutlet) {
674
+ _routerOutlet.innerHTML = "";
675
+ await runScript(vlxPath, { target: _routerOutlet });
676
+ }
677
+ }
678
+
679
+ function routerStart(outlet) {
680
+ _routerOutlet = outlet;
681
+ const go = () => {
682
+ const route = window.location.hash.slice(1) || "/";
683
+ routerNavigate(route);
684
+ };
685
+ window.addEventListener("hashchange", go);
686
+ go(); // run on start
687
+ }
688
+
689
+ // ── $on event binder ──────────────────────────────────────────────────────
690
+
691
+ async function bindEvent(cmd, options = {}) {
692
+ const { event, selector, call } = cmd;
693
+ const els = selector === "." || selector === "window"
694
+ ? [window]
695
+ : Array.from(document.querySelectorAll(selectorToCss(selector)));
696
+
697
+ if (!els.length) {
698
+ warn(\`$on \${event}: no elements match "\${selector}"\`);
699
+ return;
700
+ }
701
+
702
+ const domEvent = event === "hover" ? "mouseenter" : event;
703
+
704
+ for (const el of els) {
705
+ const handler = async (e) => {
706
+ if (event === "submit") e.preventDefault();
707
+ const target = resolveTarget(options.target || null) || el;
708
+
709
+ // Inline call inside $on
710
+ const stmt = parseStatement(call);
711
+ if (stmt) await executeCommand(stmt, { target, event: e, sourceEl: el });
712
+ };
713
+ el.addEventListener(domEvent, handler);
714
+ }
715
+ }
716
+
717
+ // Convert Velox selector syntax to a CSS selector string
718
+ function selectorToCss(sel) {
719
+ if (!sel) return "body";
720
+ const idM = sel.match(/^\\$id\\(([^)]+)\\)$/);
721
+ const clsM = sel.match(/^\\$class\\(([^)]+)\\)$/);
722
+ const qM = sel.match(/^\\$query\\((.+)\\)$/);
723
+ const tagM = sel.match(/^\\$tag\\(([^)]+)\\)$/);
724
+ if (idM) return \`#\${idM[1]}\`;
725
+ if (clsM) return \`.\${clsM[1]}\`;
726
+ if (qM) return qM[1];
727
+ if (tagM) return tagM[1];
728
+ return sel;
729
+ }
730
+
731
+ // ── Command executor ───────────────────────────────────────────────────────
732
+
733
+ async function executeCommand(cmd, options = {}) {
734
+ // Per-command $target overrides the options target
735
+ const targetSel = cmd.target || options.target;
736
+ const target = typeof targetSel === "string"
737
+ ? resolveTarget(targetSel)
738
+ : (targetSel instanceof Element ? targetSel : resolveTarget(options.target));
739
+
740
+ // ── $on event binding ──────────────────────────────────────────────────
741
+ if (cmd.fn === "$on") {
742
+ await bindEvent(cmd, options);
743
+ return;
744
+ }
745
+
746
+ const execute = async () => {
747
+ switch (cmd.fn) {
748
+
749
+ // ── DOM ─────────────────────────────────────────────────────────────
750
+
751
+ case "Dom.setText":
752
+ if (target) target.textContent = cmd.arg;
753
+ evalBlocks(cmd.blocks, !!target, target); break;
754
+
755
+ case "Dom.setHtml":
756
+ if (target) target.innerHTML = cmd.arg;
757
+ evalBlocks(cmd.blocks, !!target, target); break;
758
+
759
+ case "Dom.append": {
760
+ if (target) {
761
+ const tpl = document.createElement("template");
762
+ tpl.innerHTML = cmd.arg;
763
+ target.appendChild(tpl.content.cloneNode(true));
764
+ }
765
+ evalBlocks(cmd.blocks, !!target, target); break;
766
+ }
767
+
768
+ case "Dom.prepend": {
769
+ if (target) {
770
+ const tpl = document.createElement("template");
771
+ tpl.innerHTML = cmd.arg;
772
+ target.prepend(tpl.content.cloneNode(true));
773
+ }
774
+ evalBlocks(cmd.blocks, !!target, target); break;
775
+ }
776
+
777
+ case "Dom.remove":
778
+ if (target && target !== document.body) target.remove();
779
+ evalBlocks(cmd.blocks, true, null); break;
780
+
781
+ case "Dom.clear":
782
+ if (target) target.innerHTML = "";
783
+ evalBlocks(cmd.blocks, !!target, target); break;
784
+
785
+ case "Dom.show":
786
+ if (target) target.style.display = "";
787
+ evalBlocks(cmd.blocks, true, target); break;
788
+
789
+ case "Dom.hide":
790
+ if (target) target.style.display = "none";
791
+ evalBlocks(cmd.blocks, true, target); break;
792
+
793
+ case "Dom.toggle":
794
+ if (target) target.style.display = target.style.display === "none" ? "" : "none";
795
+ evalBlocks(cmd.blocks, true, target); break;
796
+
797
+ case "Dom.addClass":
798
+ if (target) target.classList.add(cmd.arg);
799
+ evalBlocks(cmd.blocks, true, target); break;
800
+
801
+ case "Dom.removeClass":
802
+ if (target) target.classList.remove(cmd.arg);
803
+ evalBlocks(cmd.blocks, true, target); break;
804
+
805
+ case "Dom.toggleClass":
806
+ if (target) target.classList.toggle(cmd.arg);
807
+ evalBlocks(cmd.blocks, true, target); break;
808
+
809
+ case "Dom.setAttr": {
810
+ if (target) {
811
+ const [attr, ...vals] = cmd.arg.split(":");
812
+ target.setAttribute(attr.trim(), vals.join(":").trim());
813
+ }
814
+ evalBlocks(cmd.blocks, !!target, target); break;
815
+ }
816
+
817
+ case "Dom.removeAttr":
818
+ if (target) target.removeAttribute(cmd.arg);
819
+ evalBlocks(cmd.blocks, !!target, target); break;
820
+
821
+ case "Dom.setStyle": {
822
+ if (target) {
823
+ const colonIdx = cmd.arg.indexOf(":");
824
+ const prop = cmd.arg.slice(0, colonIdx).trim();
825
+ const val = cmd.arg.slice(colonIdx + 1).trim();
826
+ target.style[prop] = val;
827
+ }
828
+ evalBlocks(cmd.blocks, !!target, target); break;
829
+ }
830
+
831
+ case "Dom.focus":
832
+ if (target) target.focus();
833
+ evalBlocks(cmd.blocks, !!target, target); break;
834
+
835
+ case "Dom.click":
836
+ if (target) target.click();
837
+ evalBlocks(cmd.blocks, !!target, target); break;
838
+
839
+ case "Dom.scrollTo":
840
+ if (target) target.scrollIntoView({ behavior: "smooth" });
841
+ evalBlocks(cmd.blocks, !!target, target); break;
842
+
843
+ case "Dom.getValue":
844
+ log(\`Dom.getValue: \${target?.value ?? "undefined"}\`);
845
+ evalBlocks(cmd.blocks, target?.value !== undefined, target); break;
846
+
847
+ case "Dom.setValue":
848
+ if (target) target.value = cmd.arg;
849
+ evalBlocks(cmd.blocks, !!target, target); break;
850
+
851
+ // ── ANIMATION ───────────────────────────────────────────────────────
852
+
853
+ case "Anim.fadeIn": {
854
+ if (target) animFadeIn(target, parseInt(cmd.arg) || 300);
855
+ evalBlocks(cmd.blocks, !!target, target); break;
856
+ }
857
+
858
+ case "Anim.fadeOut": {
859
+ if (target) animFadeOut(target, parseInt(cmd.arg) || 300);
860
+ evalBlocks(cmd.blocks, !!target, target); break;
861
+ }
862
+
863
+ case "Anim.slide": {
864
+ if (target) {
865
+ const [dir, ms] = cmd.arg.split(":");
866
+ animSlide(target, dir.trim(), parseInt(ms) || 300);
867
+ }
868
+ evalBlocks(cmd.blocks, !!target, target); break;
869
+ }
870
+
871
+ case "Anim.shake":
872
+ if (target) animShake(target);
873
+ evalBlocks(cmd.blocks, !!target, target); break;
874
+
875
+ case "Anim.pulse":
876
+ if (target) animPulse(target, parseInt(cmd.arg) || 600);
877
+ evalBlocks(cmd.blocks, !!target, target); break;
878
+
879
+ // ── FORM ────────────────────────────────────────────────────────────
880
+
881
+ case "Form.getValue": {
882
+ const el = resolveTarget(cmd.arg);
883
+ log(\`Form.getValue: \${el?.value ?? "undefined"}\`);
884
+ evalBlocks(cmd.blocks, el?.value !== undefined, target); break;
885
+ }
886
+
887
+ case "Form.setValue": {
888
+ const colonIdx = cmd.arg.indexOf(":");
889
+ const sel = cmd.arg.slice(0, colonIdx).trim();
890
+ const val = cmd.arg.slice(colonIdx + 1).trim();
891
+ const el = resolveTarget(sel);
892
+ if (el) el.value = val;
893
+ evalBlocks(cmd.blocks, !!el, target); break;
894
+ }
895
+
896
+ case "Form.clear": {
897
+ const form = resolveTarget(cmd.arg) || target;
898
+ if (form) form.querySelectorAll("input,textarea,select").forEach(i => { i.value = ""; });
899
+ evalBlocks(cmd.blocks, !!form, target); break;
900
+ }
901
+
902
+ case "Form.serialize": {
903
+ const form = resolveTarget(cmd.arg) || target;
904
+ if (form) {
905
+ const data = {};
906
+ form.querySelectorAll("[name]").forEach(i => { data[i.name] = i.value; });
907
+ log(\`Form.serialize: \${JSON.stringify(data)}\`);
908
+ if (target && target !== form) target.textContent = JSON.stringify(data, null, 2);
909
+ }
910
+ evalBlocks(cmd.blocks, !!form, target); break;
911
+ }
912
+
913
+ // ── LOCAL STORAGE ────────────────────────────────────────────────────
914
+
915
+ case "Store.set": {
916
+ const colonIdx = cmd.arg.indexOf(":");
917
+ const k = cmd.arg.slice(0, colonIdx).trim();
918
+ const v = cmd.arg.slice(colonIdx + 1).trim();
919
+ localStorage.setItem(k, v);
920
+ log(\`Store.set: \${k} = \${v}\`);
921
+ evalBlocks(cmd.blocks, true, target); break;
922
+ }
923
+
924
+ case "Store.get": {
925
+ const val = localStorage.getItem(cmd.arg);
926
+ log(\`Store.get: \${cmd.arg} = \${val ?? "null"}\`);
927
+ if (target && val !== null) target.textContent = val;
928
+ evalBlocks(cmd.blocks, val !== null, target); break;
929
+ }
930
+
931
+ case "Store.remove":
932
+ localStorage.removeItem(cmd.arg);
933
+ evalBlocks(cmd.blocks, true, target); break;
934
+
935
+ case "Store.clear":
936
+ localStorage.clear();
937
+ evalBlocks(cmd.blocks, true, target); break;
938
+
939
+ // ── COOKIES ─────────────────────────────────────────────────────────
940
+
941
+ case "Cookie.set": {
942
+ const colonIdx = cmd.arg.indexOf(":");
943
+ const k = cmd.arg.slice(0, colonIdx).trim();
944
+ const v = cmd.arg.slice(colonIdx + 1).trim();
945
+ cookieSet(k, v);
946
+ evalBlocks(cmd.blocks, true, target); break;
947
+ }
948
+
949
+ case "Cookie.get": {
950
+ const val = cookieGet(cmd.arg);
951
+ log(\`Cookie.get: \${cmd.arg} = \${val ?? "null"}\`);
952
+ if (target && val !== null) target.textContent = val;
953
+ evalBlocks(cmd.blocks, val !== null, target); break;
954
+ }
955
+
956
+ case "Cookie.remove":
957
+ cookieRemove(cmd.arg);
958
+ evalBlocks(cmd.blocks, true, target); break;
959
+
960
+ // ── VARIABLES ────────────────────────────────────────────────────────
961
+
962
+ case "Var.set": {
963
+ const colonIdx = cmd.arg.indexOf(":");
964
+ const k = cmd.arg.slice(0, colonIdx).trim();
965
+ const v = cmd.arg.slice(colonIdx + 1).trim();
966
+ _vars[k] = v;
967
+ log(\`Var.set: \${k} = \${v}\`);
968
+ evalBlocks(cmd.blocks, true, target); break;
969
+ }
970
+
971
+ case "Var.get": {
972
+ const val = _vars[cmd.arg];
973
+ log(\`Var.get: \${cmd.arg} = \${val ?? "undefined"}\`);
974
+ if (target && val !== undefined) target.textContent = val;
975
+ evalBlocks(cmd.blocks, val !== undefined, target); break;
976
+ }
977
+
978
+ case "Var.clear":
979
+ if (cmd.arg === "*") _vars = {};
980
+ else delete _vars[cmd.arg];
981
+ evalBlocks(cmd.blocks, true, target); break;
982
+
983
+ // ── NETWORK ─────────────────────────────────────────────────────────
984
+
985
+ case "iframe.Url": {
986
+ const url = resolvePath(cmd.arg);
987
+ let iframeEl;
988
+ if (target && target.tagName === "IFRAME") {
989
+ iframeEl = target;
990
+ } else {
991
+ iframeEl = document.createElement("iframe");
992
+ iframeEl.style.cssText = "width:100%;height:600px;border:none;display:block;";
993
+ (target || document.body).appendChild(iframeEl);
994
+ }
995
+ iframeEl.src = url;
996
+ evalBlocks(cmd.blocks, true, iframeEl);
997
+ break;
998
+ }
999
+
1000
+ case "Url.fetch": {
1001
+ const url = resolvePath(cmd.arg);
1002
+ try {
1003
+ const res = await fetch(url);
1004
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
1005
+ if (cmd.type === "download") {
1006
+ const blob = await res.blob();
1007
+ const a = document.createElement("a");
1008
+ a.href = URL.createObjectURL(blob);
1009
+ a.download = url.split("/").pop();
1010
+ a.click();
1011
+ }
1012
+ evalBlocks(cmd.blocks, true, target);
1013
+ } catch (e) {
1014
+ warn(\`Url.fetch failed: \${e.message}\`);
1015
+ evalBlocks(cmd.blocks, false, target);
1016
+ }
1017
+ break;
1018
+ }
1019
+
1020
+ case "Url.get": {
1021
+ const url = resolvePath(cmd.arg);
1022
+ try {
1023
+ const res = await fetch(url);
1024
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
1025
+ const text = await res.text();
1026
+ if (target) target.textContent = text;
1027
+ evalBlocks(cmd.blocks, true, target);
1028
+ } catch (e) {
1029
+ warn(\`Url.get failed: \${e.message}\`);
1030
+ evalBlocks(cmd.blocks, false, target);
1031
+ }
1032
+ break;
1033
+ }
1034
+
1035
+ case "Url.getJson": {
1036
+ const url = resolvePath(cmd.arg);
1037
+ try {
1038
+ const res = await fetch(url);
1039
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
1040
+ const json = await res.json();
1041
+ if (target) target.textContent = JSON.stringify(json, null, 2);
1042
+ evalBlocks(cmd.blocks, true, target);
1043
+ } catch (e) {
1044
+ warn(\`Url.getJson failed: \${e.message}\`);
1045
+ evalBlocks(cmd.blocks, false, target);
1046
+ }
1047
+ break;
1048
+ }
1049
+
1050
+ case "Url.post": {
1051
+ const url = resolvePath(cmd.arg);
1052
+ try {
1053
+ const res = await fetch(url, { method: "POST" });
1054
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
1055
+ evalBlocks(cmd.blocks, true, target);
1056
+ } catch (e) {
1057
+ warn(\`Url.post failed: \${e.message}\`);
1058
+ evalBlocks(cmd.blocks, false, target);
1059
+ }
1060
+ break;
1061
+ }
1062
+
1063
+ case "Url.postJson": {
1064
+ // arg format: "url:jsonBody"
1065
+ const colonIdx = cmd.arg.indexOf(":");
1066
+ const url = resolvePath(cmd.arg.slice(0, colonIdx).trim());
1067
+ const body = cmd.arg.slice(colonIdx + 1).trim();
1068
+ try {
1069
+ const res = await fetch(url, {
1070
+ method: "POST",
1071
+ headers: { "Content-Type": "application/json" },
1072
+ body,
1073
+ });
1074
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
1075
+ evalBlocks(cmd.blocks, true, target);
1076
+ } catch (e) {
1077
+ warn(\`Url.postJson failed: \${e.message}\`);
1078
+ evalBlocks(cmd.blocks, false, target);
1079
+ }
1080
+ break;
1081
+ }
1082
+
1083
+ case "file.localFetch": {
1084
+ const url = resolvePath(cmd.arg);
1085
+ try {
1086
+ const res = await fetch(url);
1087
+ if (!res.ok) throw new Error("Not found");
1088
+ if (cmd.type === "download") {
1089
+ const blob = await res.blob();
1090
+ const a = document.createElement("a");
1091
+ a.href = URL.createObjectURL(blob);
1092
+ a.download = url.split("/").pop();
1093
+ a.click();
1094
+ }
1095
+ evalBlocks(cmd.blocks, true, target);
1096
+ } catch (e) {
1097
+ warn(\`file.localFetch failed: \${e.message}\`);
1098
+ evalBlocks(cmd.blocks, false, target);
1099
+ }
1100
+ break;
1101
+ }
1102
+
1103
+ // ── PAGE ─────────────────────────────────────────────────────────────
1104
+
1105
+ case "Page.redirect":
1106
+ evalBlocks(cmd.blocks, true, target);
1107
+ window.location.href = resolvePath(cmd.arg);
1108
+ break;
1109
+
1110
+ case "Page.reload":
1111
+ evalBlocks(cmd.blocks, true, target);
1112
+ window.location.reload();
1113
+ break;
1114
+
1115
+ case "Page.title":
1116
+ document.title = cmd.arg;
1117
+ evalBlocks(cmd.blocks, true, target);
1118
+ break;
1119
+
1120
+ case "Page.meta": {
1121
+ const colonIdx = cmd.arg.indexOf(":");
1122
+ const name = cmd.arg.slice(0, colonIdx).trim();
1123
+ const content = cmd.arg.slice(colonIdx + 1).trim();
1124
+ let meta = document.querySelector(\`meta[name="\${name}"]\`);
1125
+ if (!meta) { meta = document.createElement("meta"); meta.name = name; document.head.appendChild(meta); }
1126
+ meta.content = content;
1127
+ evalBlocks(cmd.blocks, true, target);
1128
+ break;
1129
+ }
1130
+
1131
+ case "Page.scrollTop":
1132
+ window.scrollTo({ top: 0, behavior: "smooth" });
1133
+ evalBlocks(cmd.blocks, true, target);
1134
+ break;
1135
+
1136
+ // ── ROUTER ───────────────────────────────────────────────────────────
1137
+
1138
+ case "Router.define": {
1139
+ const colonIdx = cmd.arg.indexOf(":");
1140
+ const route = cmd.arg.slice(0, colonIdx).trim();
1141
+ const vlxPath = cmd.arg.slice(colonIdx + 1).trim();
1142
+ _routes[route] = vlxPath;
1143
+ evalBlocks(cmd.blocks, true, target);
1144
+ break;
1145
+ }
1146
+
1147
+ case "Router.go":
1148
+ await routerNavigate(cmd.arg);
1149
+ evalBlocks(cmd.blocks, true, target);
1150
+ break;
1151
+
1152
+ case "Router.start":
1153
+ routerStart(target || document.body);
1154
+ evalBlocks(cmd.blocks, true, target);
1155
+ break;
1156
+
1157
+ // ── TEMPLATE ─────────────────────────────────────────────────────────
1158
+
1159
+ case "Template.render": {
1160
+ // arg: "templateId:jsonData"
1161
+ const colonIdx = cmd.arg.indexOf(":");
1162
+ const tplId = cmd.arg.slice(0, colonIdx).trim();
1163
+ let data = {};
1164
+ try { data = JSON.parse(cmd.arg.slice(colonIdx + 1).trim()); } catch (_) {}
1165
+ const ok = renderTemplate(tplId, data, target);
1166
+ evalBlocks(cmd.blocks, ok, target);
1167
+ break;
1168
+ }
1169
+
1170
+ // ── SCRIPT / STYLE ───────────────────────────────────────────────────
1171
+
1172
+ case "Script.load": {
1173
+ const url = resolvePath(cmd.arg);
1174
+ try {
1175
+ await new Promise((resolve, reject) => {
1176
+ const s = document.createElement("script");
1177
+ s.src = url; s.onload = resolve; s.onerror = reject;
1178
+ document.head.appendChild(s);
1179
+ });
1180
+ evalBlocks(cmd.blocks, true, target);
1181
+ } catch (e) {
1182
+ warn(\`Script.load failed: \${e.message}\`);
1183
+ evalBlocks(cmd.blocks, false, target);
1184
+ }
1185
+ break;
1186
+ }
1187
+
1188
+ case "Style.load": {
1189
+ const link = document.createElement("link");
1190
+ link.rel = "stylesheet";
1191
+ link.href = resolvePath(cmd.arg);
1192
+ document.head.appendChild(link);
1193
+ evalBlocks(cmd.blocks, true, target);
1194
+ break;
1195
+ }
1196
+
1197
+ // ── ECHO / LOGGING ────────────────────────────────────────────────────
1198
+
1199
+ case "echo": log(cmd.arg); break;
1200
+ case "echo.warn": warn(cmd.arg); break;
1201
+ case "echo.err": err(cmd.arg); break;
1202
+
1203
+ default:
1204
+ warn(\`Unknown command: \${cmd.fn}\`);
1205
+ }
1206
+ };
1207
+
1208
+ // Delay + repeat support
1209
+ if (cmd.delay) await sleep(cmd.delay);
1210
+ for (let i = 0; i < (cmd.repeat || 1); i++) {
1211
+ if (i > 0 && cmd.delay) await sleep(cmd.delay);
1212
+ await execute();
1213
+ }
1214
+ }
1215
+
1216
+ // ── Block evaluator ────────────────────────────────────────────────────────
1217
+
1218
+ function evalBlocks(blocks, success, target) {
1219
+ let handled = false;
1220
+ for (const block of blocks) {
1221
+ switch (block.keyword) {
1222
+ case "if": {
1223
+ const cond = block.value === null ? success : (block.value === true || block.value === "true");
1224
+ if (cond) { runCall(block.call, target); handled = true; }
1225
+ break;
1226
+ }
1227
+ case "if else": {
1228
+ if (!handled) {
1229
+ const cond = block.value === null ? success : (block.value === true || block.value === "true");
1230
+ if (cond) { runCall(block.call, target); handled = true; }
1231
+ }
1232
+ break;
1233
+ }
1234
+ case "else":
1235
+ if (!handled) { runCall(block.call, target); handled = true; }
1236
+ break;
1237
+ case "log":
1238
+ runCall(block.call, target); break;
1239
+ }
1240
+ }
1241
+ }
1242
+
1243
+ // ── Inline callback runner (used inside $if/$else blocks) ─────────────────
1244
+
1245
+ function runCall(callStr, target) {
1246
+ if (!callStr) return;
1247
+ callStr = callStr.trim();
1248
+
1249
+ // echo / echo.err / echo.warn
1250
+ const echoMatch = callStr.match(/^echo(?:\\.(err|warn))?\\("([^"]*)"\\)$/);
1251
+ if (echoMatch) {
1252
+ const type = echoMatch[1], msg = echoMatch[2];
1253
+ if (type === "err") err(msg);
1254
+ else if (type === "warn") warn(msg);
1255
+ else log(msg);
1256
+ return;
1257
+ }
1258
+
1259
+ // Dom calls
1260
+ const domTextMatch = callStr.match(/^Dom\\.(setText|setHtml)\\("([^"]*)"\\)$/);
1261
+ if (domTextMatch && target) {
1262
+ if (domTextMatch[1] === "setText") target.textContent = domTextMatch[2];
1263
+ else target.innerHTML = domTextMatch[2];
1264
+ return;
1265
+ }
1266
+
1267
+ const domVisMatch = callStr.match(/^Dom\\.(show|hide|toggle|remove|clear|focus|click|scrollTo)\\(\\)$/);
1268
+ if (domVisMatch && target) {
1269
+ switch (domVisMatch[1]) {
1270
+ case "show": target.style.display = ""; break;
1271
+ case "hide": target.style.display = "none"; break;
1272
+ case "toggle": target.style.display = target.style.display === "none" ? "" : "none"; break;
1273
+ case "remove": target.remove(); break;
1274
+ case "clear": target.innerHTML = ""; break;
1275
+ case "focus": target.focus(); break;
1276
+ case "click": target.click(); break;
1277
+ case "scrollTo": target.scrollIntoView({ behavior: "smooth" }); break;
1278
+ }
1279
+ return;
1280
+ }
1281
+
1282
+ const domClassMatch = callStr.match(/^Dom\\.(addClass|removeClass|toggleClass)\\("([^"]*)"\\)$/);
1283
+ if (domClassMatch && target) {
1284
+ if (domClassMatch[1] === "addClass") target.classList.add(domClassMatch[2]);
1285
+ else if (domClassMatch[1] === "removeClass") target.classList.remove(domClassMatch[2]);
1286
+ else target.classList.toggle(domClassMatch[2]);
1287
+ return;
1288
+ }
1289
+
1290
+ const domStyleMatch = callStr.match(/^Dom\\.setStyle\\("([^"]+)"\\)$/);
1291
+ if (domStyleMatch && target) {
1292
+ const colonIdx = domStyleMatch[1].indexOf(":");
1293
+ target.style[domStyleMatch[1].slice(0, colonIdx).trim()] = domStyleMatch[1].slice(colonIdx + 1).trim();
1294
+ return;
1295
+ }
1296
+
1297
+ // Var calls
1298
+ const varSetMatch = callStr.match(/^Var\\.set\\("([^"]+)"\\)$/);
1299
+ if (varSetMatch) {
1300
+ const colonIdx = varSetMatch[1].indexOf(":");
1301
+ _vars[varSetMatch[1].slice(0, colonIdx).trim()] = varSetMatch[1].slice(colonIdx + 1).trim();
1302
+ return;
1303
+ }
1304
+
1305
+ // Anim calls
1306
+ const animMatch = callStr.match(/^Anim\\.(fadeIn|fadeOut|shake|pulse)\\("?([^"]*)"?\\)$/);
1307
+ if (animMatch && target) {
1308
+ const [, fn, arg] = animMatch;
1309
+ if (fn === "fadeIn") animFadeIn(target, parseInt(arg) || 300);
1310
+ if (fn === "fadeOut") animFadeOut(target, parseInt(arg) || 300);
1311
+ if (fn === "shake") animShake(target);
1312
+ if (fn === "pulse") animPulse(target, parseInt(arg) || 600);
1313
+ return;
1314
+ }
1315
+
1316
+ // Page calls
1317
+ if (callStr === "Page.reload()") { window.location.reload(); return; }
1318
+ if (callStr === "Page.scrollTop()") { window.scrollTo({ top: 0, behavior: "smooth" }); return; }
1319
+ const redirectMatch = callStr.match(/^Page\\.redirect\\("([^"]*)"\\)$/);
1320
+ if (redirectMatch) { window.location.href = resolvePath(redirectMatch[1]); return; }
1321
+
1322
+ warn(\`Unknown callback: \${callStr}\`);
1323
+ }
1324
+
1325
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
1326
+
1327
+ // ── Run a .vlx file ────────────────────────────────────────────────────────
1328
+
1329
+ async function runScript(vlxPath, options = {}) {
1330
+ const resolved = resolvePath(vlxPath);
1331
+ try {
1332
+ const res = await fetch(resolved);
1333
+ if (!res.ok) throw new Error(\`Could not load \${resolved} (HTTP \${res.status})\`);
1334
+ const source = await res.text();
1335
+ const commands = parseVlx(source);
1336
+ for (const cmd of commands) await executeCommand(cmd, options);
1337
+ } catch (e) {
1338
+ err("Runtime error: " + e.message);
1339
+ }
1340
+ }
1341
+
1342
+ // ── Run inline .vlx source ─────────────────────────────────────────────────
1343
+
1344
+ async function runInline(source, options = {}) {
1345
+ const commands = parseVlx(source);
1346
+ for (const cmd of commands) await executeCommand(cmd, options);
1347
+ }
1348
+
1349
+ // ── Auto-init: scan HTML for <?velox ... > tags ────────────────────────────
1350
+
1351
+ async function autoInit() {
1352
+ await loadConfig();
1353
+ await loadQuickPaths();
1354
+ await loadVars();
1355
+
1356
+ // Read raw HTML source to find <?velox > tags
1357
+ const html = document.documentElement.outerHTML;
1358
+ const tags = extractVeloxTags(html);
1359
+
1360
+ for (const tag of tags) {
1361
+ if (!tag) continue;
1362
+
1363
+ // Velox.run("path") [$target="..."]
1364
+ const inline = parseInlineTag(tag);
1365
+ if (inline && inline.kind === "run") {
1366
+ const resolvedTarget = resolveTarget(inline.target);
1367
+ evalBlocks(inline.blocks, true, resolvedTarget);
1368
+ await runScript(inline.vlxPath, { target: resolvedTarget, type: inline.type });
1369
+ continue;
1370
+ }
1371
+
1372
+ // Raw .vlx syntax directly in the tag
1373
+ // Parse out optional $target="..." at the tag level
1374
+ const tagTargetMatch = tag.match(/\\$target\\s*=\\s*"([^"]+)"/);
1375
+ const tagTarget = tagTargetMatch ? resolveTarget(tagTargetMatch[1]) : null;
1376
+ await runInline(tag, { target: tagTarget });
1377
+ }
1378
+ }
1379
+
1380
+ // ── Boot ───────────────────────────────────────────────────────────────────
1381
+
1382
+ if (document.readyState === "loading") {
1383
+ document.addEventListener("DOMContentLoaded", autoInit);
1384
+ } else {
1385
+ autoInit();
1386
+ }
1387
+
1388
+ // ── Public API ─────────────────────────────────────────────────────────────
1389
+
1390
+ return {
1391
+ run: runScript,
1392
+ runInline,
1393
+ var: (key) => _vars[key] ?? null,
1394
+ navigate: routerNavigate,
1395
+ };
1396
+
1397
+ })();
1398
+ `,
1399
+
1400
+ };