@constela/runtime 0.13.0 → 0.14.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/dist/index.d.ts CHANGED
@@ -275,6 +275,7 @@ interface ActionContext {
275
275
  eventPayload?: unknown;
276
276
  refs?: Record<string, Element>;
277
277
  subscriptions?: (() => void)[];
278
+ cleanups?: (() => void)[];
278
279
  route?: {
279
280
  params: Record<string, string>;
280
281
  query: Record<string, string>;
package/dist/index.js CHANGED
@@ -428,6 +428,18 @@ function evaluate(expr, ctx) {
428
428
  return val == null ? "" : String(val);
429
429
  }).join("");
430
430
  }
431
+ case "validity": {
432
+ const element2 = ctx.refs?.[expr.ref];
433
+ if (!element2) return null;
434
+ const formElement = element2;
435
+ if (!formElement.validity) return null;
436
+ const validity = formElement.validity;
437
+ const property = expr.property || "valid";
438
+ if (property === "message") {
439
+ return formElement.validationMessage || "";
440
+ }
441
+ return validity[property] ?? null;
442
+ }
431
443
  default: {
432
444
  const _exhaustiveCheck = expr;
433
445
  throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
@@ -593,15 +605,24 @@ async function executeAction(action, ctx) {
593
605
  const extAction = action;
594
606
  const isLocal = extAction._isLocalAction && extAction._localStore;
595
607
  const localStore = extAction._localStore;
608
+ const delayPromises = [];
596
609
  for (const step of action.steps) {
597
610
  if (step.do === "set" || step.do === "update" || step.do === "setPath") {
598
611
  executeStepSync(step, ctx, isLocal ? localStore : void 0);
599
612
  } else if (step.do === "if") {
600
613
  await executeIfStep(step, ctx, isLocal ? localStore : void 0);
614
+ } else if (step.do === "delay") {
615
+ const delayPromise = executeDelayStep(step, ctx);
616
+ delayPromises.push(delayPromise);
617
+ } else if (step.do === "interval") {
618
+ await executeIntervalStep(step, ctx);
601
619
  } else {
602
620
  await executeStep(step, ctx);
603
621
  }
604
622
  }
623
+ if (delayPromises.length > 0) {
624
+ await Promise.all(delayPromises);
625
+ }
605
626
  }
606
627
  function executeStepSync(step, ctx, localStore) {
607
628
  switch (step.do) {
@@ -892,6 +913,18 @@ async function executeStep(step, ctx) {
892
913
  case "close":
893
914
  await executeCloseStep(step, ctx);
894
915
  break;
916
+ case "delay":
917
+ await executeDelayStep(step, ctx);
918
+ break;
919
+ case "interval":
920
+ await executeIntervalStep(step, ctx);
921
+ break;
922
+ case "clearTimer":
923
+ await executeClearTimerStep(step, ctx);
924
+ break;
925
+ case "focus":
926
+ await executeFocusStep(step, ctx);
927
+ break;
895
928
  }
896
929
  }
897
930
  async function executeSetStep(target, value, ctx) {
@@ -1258,6 +1291,137 @@ async function executeCloseStep(step, ctx) {
1258
1291
  if (!ctx.connections) return;
1259
1292
  ctx.connections.close(step.connection);
1260
1293
  }
1294
+ async function executeDelayStep(step, ctx) {
1295
+ const evalCtx = createEvalContext(ctx);
1296
+ const msValue = evaluate(step.ms, evalCtx);
1297
+ const ms = typeof msValue === "number" ? Math.max(0, msValue) : 0;
1298
+ return new Promise((resolve) => {
1299
+ let resolved = false;
1300
+ const timeoutId = setTimeout(async () => {
1301
+ if (resolved) return;
1302
+ resolved = true;
1303
+ for (const thenStep of step.then) {
1304
+ if (thenStep.do === "set" || thenStep.do === "update" || thenStep.do === "setPath") {
1305
+ executeStepSync(thenStep, ctx);
1306
+ } else if (thenStep.do === "if") {
1307
+ await executeIfStep(thenStep, ctx);
1308
+ } else {
1309
+ await executeStep(thenStep, ctx);
1310
+ }
1311
+ }
1312
+ resolve();
1313
+ }, ms);
1314
+ const numericId = typeof timeoutId === "number" ? timeoutId : Number(timeoutId);
1315
+ if (step.result) {
1316
+ ctx.locals[step.result] = numericId;
1317
+ }
1318
+ if (!ctx.locals["_timerResolvers"]) {
1319
+ ctx.locals["_timerResolvers"] = /* @__PURE__ */ new Map();
1320
+ }
1321
+ ctx.locals["_timerResolvers"].set(numericId, () => {
1322
+ if (!resolved) {
1323
+ resolved = true;
1324
+ clearTimeout(timeoutId);
1325
+ resolve();
1326
+ }
1327
+ });
1328
+ if (ctx.cleanups) {
1329
+ ctx.cleanups.push(() => {
1330
+ if (!resolved) {
1331
+ resolved = true;
1332
+ clearTimeout(timeoutId);
1333
+ resolve();
1334
+ }
1335
+ });
1336
+ }
1337
+ });
1338
+ }
1339
+ async function executeIntervalStep(step, ctx) {
1340
+ const evalCtx = createEvalContext(ctx);
1341
+ const msValue = evaluate(step.ms, evalCtx);
1342
+ const ms = typeof msValue === "number" ? Math.max(0, msValue) : 0;
1343
+ const intervalId = setInterval(async () => {
1344
+ const action = ctx.actions[step.action];
1345
+ if (action) {
1346
+ await executeAction(action, ctx);
1347
+ }
1348
+ }, ms);
1349
+ const numericId = typeof intervalId === "number" ? intervalId : Number(intervalId);
1350
+ if (step.result) {
1351
+ ctx.locals[step.result] = numericId;
1352
+ }
1353
+ if (ctx.cleanups) {
1354
+ ctx.cleanups.push(() => clearInterval(intervalId));
1355
+ }
1356
+ }
1357
+ async function executeClearTimerStep(step, ctx) {
1358
+ const evalCtx = createEvalContext(ctx);
1359
+ const timerId = evaluate(step.target, evalCtx);
1360
+ if (timerId == null) {
1361
+ return;
1362
+ }
1363
+ const numericId = typeof timerId === "number" ? timerId : Number(timerId);
1364
+ const timerResolvers = ctx.locals["_timerResolvers"];
1365
+ if (timerResolvers?.has(numericId)) {
1366
+ const resolver = timerResolvers.get(numericId);
1367
+ if (resolver) {
1368
+ resolver();
1369
+ }
1370
+ timerResolvers.delete(numericId);
1371
+ }
1372
+ clearTimeout(timerId);
1373
+ clearInterval(timerId);
1374
+ }
1375
+ async function executeFocusStep(step, ctx) {
1376
+ const evalCtx = createEvalContext(ctx);
1377
+ const targetValue = evaluate(step.target, evalCtx);
1378
+ let element2;
1379
+ if (targetValue instanceof Element) {
1380
+ element2 = targetValue;
1381
+ } else if (typeof targetValue === "string") {
1382
+ element2 = ctx.refs?.[targetValue];
1383
+ }
1384
+ try {
1385
+ if (!element2) {
1386
+ const refName = typeof targetValue === "string" ? targetValue : "unknown";
1387
+ throw new Error(`Ref "${refName}" not found`);
1388
+ }
1389
+ switch (step.operation) {
1390
+ case "focus":
1391
+ if (typeof element2.focus === "function") {
1392
+ element2.focus();
1393
+ }
1394
+ break;
1395
+ case "blur":
1396
+ if (typeof element2.blur === "function") {
1397
+ element2.blur();
1398
+ }
1399
+ break;
1400
+ case "select":
1401
+ if (typeof element2.select === "function") {
1402
+ element2.select();
1403
+ } else {
1404
+ throw new Error(`Element does not support select operation`);
1405
+ }
1406
+ break;
1407
+ }
1408
+ if (step.onSuccess) {
1409
+ for (const successStep of step.onSuccess) {
1410
+ await executeStep(successStep, ctx);
1411
+ }
1412
+ }
1413
+ } catch (err) {
1414
+ ctx.locals["error"] = {
1415
+ message: err instanceof Error ? err.message : String(err),
1416
+ name: err instanceof Error ? err.name : "Error"
1417
+ };
1418
+ if (step.onError) {
1419
+ for (const errorStep of step.onError) {
1420
+ await executeStep(errorStep, ctx);
1421
+ }
1422
+ }
1423
+ }
1424
+ }
1261
1425
 
1262
1426
  // ../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js
1263
1427
  function L() {
@@ -2691,14 +2855,14 @@ function createDOMPurify() {
2691
2855
  DocumentFragment,
2692
2856
  HTMLTemplateElement,
2693
2857
  Node: Node2,
2694
- Element,
2858
+ Element: Element2,
2695
2859
  NodeFilter,
2696
2860
  NamedNodeMap = window2.NamedNodeMap || window2.MozNamedAttrMap,
2697
2861
  HTMLFormElement,
2698
2862
  DOMParser,
2699
2863
  trustedTypes
2700
2864
  } = window2;
2701
- const ElementPrototype = Element.prototype;
2865
+ const ElementPrototype = Element2.prototype;
2702
2866
  const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
2703
2867
  const remove = lookupGetter(ElementPrototype, "remove");
2704
2868
  const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
@@ -3152,7 +3316,7 @@ function createDOMPurify() {
3152
3316
  _forceRemove(currentNode);
3153
3317
  return true;
3154
3318
  }
3155
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
3319
+ if (currentNode instanceof Element2 && !_checkValidNamespace(currentNode)) {
3156
3320
  _forceRemove(currentNode);
3157
3321
  return true;
3158
3322
  }
@@ -13248,6 +13412,188 @@ function isSvgTag(tag) {
13248
13412
  function isEventHandler(value) {
13249
13413
  return typeof value === "object" && value !== null && "event" in value && "action" in value;
13250
13414
  }
13415
+ function debounce(fn, wait, ctx) {
13416
+ let timeoutId = null;
13417
+ const debouncedFn = (event) => {
13418
+ if (timeoutId !== null) {
13419
+ clearTimeout(timeoutId);
13420
+ }
13421
+ timeoutId = setTimeout(() => {
13422
+ timeoutId = null;
13423
+ fn(event);
13424
+ }, wait);
13425
+ };
13426
+ ctx.cleanups?.push(() => {
13427
+ if (timeoutId !== null) {
13428
+ clearTimeout(timeoutId);
13429
+ timeoutId = null;
13430
+ }
13431
+ });
13432
+ return debouncedFn;
13433
+ }
13434
+ function throttle(fn, wait, ctx) {
13435
+ let lastTime = 0;
13436
+ let timeoutId = null;
13437
+ let lastEvent = null;
13438
+ const throttledFn = (event) => {
13439
+ const now = Date.now();
13440
+ const remaining = wait - (now - lastTime);
13441
+ if (remaining <= 0) {
13442
+ if (timeoutId !== null) {
13443
+ clearTimeout(timeoutId);
13444
+ timeoutId = null;
13445
+ }
13446
+ lastTime = now;
13447
+ fn(event);
13448
+ } else {
13449
+ lastEvent = event;
13450
+ if (timeoutId === null) {
13451
+ timeoutId = setTimeout(() => {
13452
+ timeoutId = null;
13453
+ lastTime = Date.now();
13454
+ if (lastEvent) {
13455
+ fn(lastEvent);
13456
+ lastEvent = null;
13457
+ }
13458
+ }, remaining);
13459
+ }
13460
+ }
13461
+ };
13462
+ ctx.cleanups?.push(() => {
13463
+ if (timeoutId !== null) {
13464
+ clearTimeout(timeoutId);
13465
+ timeoutId = null;
13466
+ }
13467
+ });
13468
+ return throttledFn;
13469
+ }
13470
+ function createEventCallback(handler, ctx) {
13471
+ return async (event) => {
13472
+ const action = ctx.actions[handler.action];
13473
+ if (!action) return;
13474
+ const eventLocals = {};
13475
+ const target = event.target;
13476
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
13477
+ eventLocals["value"] = target.value;
13478
+ if (target instanceof HTMLInputElement && target.type === "checkbox") {
13479
+ eventLocals["checked"] = target.checked;
13480
+ }
13481
+ if (target instanceof HTMLInputElement && target.type === "file") {
13482
+ eventLocals["files"] = Array.from(target.files || []).map((f) => ({
13483
+ name: f.name,
13484
+ size: f.size,
13485
+ type: f.type,
13486
+ _file: f
13487
+ }));
13488
+ }
13489
+ }
13490
+ if (event instanceof KeyboardEvent) {
13491
+ eventLocals["key"] = event.key;
13492
+ eventLocals["code"] = event.code;
13493
+ eventLocals["ctrlKey"] = event.ctrlKey;
13494
+ eventLocals["shiftKey"] = event.shiftKey;
13495
+ eventLocals["altKey"] = event.altKey;
13496
+ eventLocals["metaKey"] = event.metaKey;
13497
+ }
13498
+ if (event instanceof MouseEvent) {
13499
+ eventLocals["clientX"] = event.clientX;
13500
+ eventLocals["clientY"] = event.clientY;
13501
+ eventLocals["pageX"] = event.pageX;
13502
+ eventLocals["pageY"] = event.pageY;
13503
+ eventLocals["button"] = event.button;
13504
+ }
13505
+ const touchEvent = event;
13506
+ if (touchEvent.touches && touchEvent.changedTouches) {
13507
+ eventLocals["touches"] = Array.from(touchEvent.touches).map((t) => ({
13508
+ clientX: t.clientX,
13509
+ clientY: t.clientY,
13510
+ pageX: t.pageX,
13511
+ pageY: t.pageY
13512
+ }));
13513
+ eventLocals["changedTouches"] = Array.from(touchEvent.changedTouches).map((t) => ({
13514
+ clientX: t.clientX,
13515
+ clientY: t.clientY,
13516
+ pageX: t.pageX,
13517
+ pageY: t.pageY
13518
+ }));
13519
+ }
13520
+ if (handler.event === "scroll" && event.target instanceof Element) {
13521
+ eventLocals["scrollTop"] = event.target.scrollTop;
13522
+ eventLocals["scrollLeft"] = event.target.scrollLeft;
13523
+ }
13524
+ let payload = void 0;
13525
+ if (handler.payload) {
13526
+ payload = evaluatePayload(handler.payload, {
13527
+ state: ctx.state,
13528
+ locals: { ...ctx.locals, ...eventLocals },
13529
+ ...ctx.imports && { imports: ctx.imports }
13530
+ });
13531
+ }
13532
+ const actionCtx = {
13533
+ state: ctx.state,
13534
+ actions: ctx.actions,
13535
+ locals: { ...ctx.locals, ...eventLocals, payload },
13536
+ eventPayload: payload
13537
+ };
13538
+ await executeAction(action, actionCtx);
13539
+ };
13540
+ }
13541
+ function wrapWithDebounceThrottle(callback, handler, ctx) {
13542
+ if (handler.debounce !== void 0 && handler.debounce >= 0) {
13543
+ return debounce(callback, handler.debounce, ctx);
13544
+ }
13545
+ if (handler.throttle !== void 0 && handler.throttle >= 0) {
13546
+ return throttle(callback, handler.throttle, ctx);
13547
+ }
13548
+ return callback;
13549
+ }
13550
+ function setupIntersectionObserver(el, handler, ctx) {
13551
+ const options = {};
13552
+ if (handler.options?.threshold !== void 0) {
13553
+ options.threshold = handler.options.threshold;
13554
+ }
13555
+ if (handler.options?.rootMargin !== void 0) {
13556
+ options.rootMargin = handler.options.rootMargin;
13557
+ }
13558
+ let hasTriggered = false;
13559
+ const observer = new IntersectionObserver((entries2) => {
13560
+ for (const entry of entries2) {
13561
+ if (entry.target !== el) continue;
13562
+ if (handler.options?.once && hasTriggered) {
13563
+ continue;
13564
+ }
13565
+ const action = ctx.actions[handler.action];
13566
+ if (!action) continue;
13567
+ const intersectLocals = {
13568
+ isIntersecting: entry.isIntersecting,
13569
+ intersectionRatio: entry.intersectionRatio
13570
+ };
13571
+ let payload = void 0;
13572
+ if (handler.payload) {
13573
+ payload = evaluatePayload(handler.payload, {
13574
+ state: ctx.state,
13575
+ locals: { ...ctx.locals, ...intersectLocals },
13576
+ ...ctx.imports && { imports: ctx.imports }
13577
+ });
13578
+ }
13579
+ const actionCtx = {
13580
+ state: ctx.state,
13581
+ actions: ctx.actions,
13582
+ locals: { ...ctx.locals, ...intersectLocals, payload },
13583
+ eventPayload: payload
13584
+ };
13585
+ executeAction(action, actionCtx);
13586
+ if (handler.options?.once) {
13587
+ hasTriggered = true;
13588
+ observer.unobserve(el);
13589
+ }
13590
+ }
13591
+ }, options);
13592
+ observer.observe(el);
13593
+ ctx.cleanups?.push(() => {
13594
+ observer.disconnect();
13595
+ });
13596
+ }
13251
13597
  function render(node, ctx) {
13252
13598
  switch (node.kind) {
13253
13599
  case "element":
@@ -13262,6 +13608,8 @@ function render(node, ctx) {
13262
13608
  return renderMarkdown(node, ctx);
13263
13609
  case "code":
13264
13610
  return renderCode(node, ctx);
13611
+ case "portal":
13612
+ return renderPortal(node, ctx);
13265
13613
  case "localState":
13266
13614
  return renderLocalState(node, ctx);
13267
13615
  default:
@@ -13284,34 +13632,13 @@ function renderElement(node, ctx) {
13284
13632
  if (isEventHandler(propValue)) {
13285
13633
  const handler = propValue;
13286
13634
  const eventName = handler.event;
13287
- el.addEventListener(eventName, async (event) => {
13288
- const action = ctx.actions[handler.action];
13289
- if (action) {
13290
- const eventLocals = {};
13291
- const target = event.target;
13292
- if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
13293
- eventLocals["value"] = target.value;
13294
- if (target instanceof HTMLInputElement && target.type === "checkbox") {
13295
- eventLocals["checked"] = target.checked;
13296
- }
13297
- }
13298
- let payload = void 0;
13299
- if (handler.payload) {
13300
- payload = evaluatePayload(handler.payload, {
13301
- state: ctx.state,
13302
- locals: { ...ctx.locals, ...eventLocals },
13303
- ...ctx.imports && { imports: ctx.imports }
13304
- });
13305
- }
13306
- const actionCtx = {
13307
- state: ctx.state,
13308
- actions: ctx.actions,
13309
- locals: { ...ctx.locals, ...eventLocals, payload },
13310
- eventPayload: payload
13311
- };
13312
- await executeAction(action, actionCtx);
13313
- }
13314
- });
13635
+ if (eventName === "intersect") {
13636
+ setupIntersectionObserver(el, handler, ctx);
13637
+ } else {
13638
+ const eventCallback = createEventCallback(handler, ctx);
13639
+ const wrappedCallback = wrapWithDebounceThrottle(eventCallback, handler, ctx);
13640
+ el.addEventListener(eventName, wrappedCallback);
13641
+ }
13315
13642
  } else {
13316
13643
  const cleanup = createEffect(() => {
13317
13644
  const value = evaluate(propValue, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
@@ -13651,6 +13978,41 @@ function renderCode(node, ctx) {
13651
13978
  ctx.cleanups?.push(cleanup);
13652
13979
  return container;
13653
13980
  }
13981
+ function renderPortal(node, ctx) {
13982
+ let targetElement = null;
13983
+ if (node.target === "body") {
13984
+ targetElement = document.body;
13985
+ } else if (node.target === "head") {
13986
+ targetElement = document.head;
13987
+ } else {
13988
+ targetElement = document.querySelector(node.target);
13989
+ }
13990
+ if (!targetElement) {
13991
+ return document.createComment("portal:target-not-found");
13992
+ }
13993
+ const portalContainer = document.createElement("div");
13994
+ portalContainer.setAttribute("data-portal", "true");
13995
+ portalContainer.style.display = "contents";
13996
+ const portalCleanups = [];
13997
+ const portalCtx = {
13998
+ ...ctx,
13999
+ cleanups: portalCleanups
14000
+ };
14001
+ for (const child of node.children) {
14002
+ const childNode = render(child, portalCtx);
14003
+ portalContainer.appendChild(childNode);
14004
+ }
14005
+ targetElement.appendChild(portalContainer);
14006
+ ctx.cleanups?.push(() => {
14007
+ for (const cleanup of portalCleanups) {
14008
+ cleanup();
14009
+ }
14010
+ if (portalContainer.parentNode) {
14011
+ portalContainer.parentNode.removeChild(portalContainer);
14012
+ }
14013
+ });
14014
+ return document.createComment("portal");
14015
+ }
13654
14016
  function createLocalStateStore(stateDefs) {
13655
14017
  const signals = {};
13656
14018
  for (const [name, def] of Object.entries(stateDefs)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Runtime DOM renderer for Constela UI framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,8 +18,8 @@
18
18
  "dompurify": "^3.3.1",
19
19
  "marked": "^17.0.1",
20
20
  "shiki": "^3.20.0",
21
- "@constela/compiler": "0.10.0",
22
- "@constela/core": "0.10.0"
21
+ "@constela/compiler": "0.11.0",
22
+ "@constela/core": "0.11.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/dompurify": "^3.2.0",
@@ -29,7 +29,7 @@
29
29
  "tsup": "^8.0.0",
30
30
  "typescript": "^5.3.0",
31
31
  "vitest": "^2.0.0",
32
- "@constela/server": "6.0.0"
32
+ "@constela/server": "7.0.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"