@constela/runtime 0.12.2 → 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
@@ -1,4 +1,4 @@
1
- import { CompiledExpression, CompiledAction, CompiledNode, CompiledProgram } from '@constela/compiler';
1
+ import { CompiledExpression, CompiledAction, CompiledNode, CompiledLocalAction, CompiledProgram } from '@constela/compiler';
2
2
 
3
3
  interface Signal<T> {
4
4
  get(): T;
@@ -254,6 +254,20 @@ declare function createConnectionManager(): ConnectionManager;
254
254
  * - navigate: Page navigation
255
255
  */
256
256
 
257
+ /**
258
+ * Local state store interface for component-level state
259
+ */
260
+ interface LocalStateStore$1 {
261
+ get(name: string): unknown;
262
+ set(name: string, value: unknown): void;
263
+ }
264
+ /**
265
+ * Extended action type with local action metadata
266
+ */
267
+ interface ExtendedAction extends CompiledAction {
268
+ _isLocalAction?: boolean;
269
+ _localStore?: LocalStateStore$1;
270
+ }
257
271
  interface ActionContext {
258
272
  state: StateStore;
259
273
  actions: Record<string, CompiledAction>;
@@ -261,6 +275,7 @@ interface ActionContext {
261
275
  eventPayload?: unknown;
262
276
  refs?: Record<string, Element>;
263
277
  subscriptions?: (() => void)[];
278
+ cleanups?: (() => void)[];
264
279
  route?: {
265
280
  params: Record<string, string>;
266
281
  query: Record<string, string>;
@@ -269,7 +284,7 @@ interface ActionContext {
269
284
  imports?: Record<string, unknown>;
270
285
  connections?: ConnectionManager;
271
286
  }
272
- declare function executeAction(action: CompiledAction, ctx: ActionContext): Promise<void>;
287
+ declare function executeAction(action: CompiledAction | ExtendedAction, ctx: ActionContext): Promise<void>;
273
288
 
274
289
  /**
275
290
  * Renderer - DOM rendering for compiled view nodes
@@ -281,6 +296,14 @@ declare function executeAction(action: CompiledAction, ctx: ActionContext): Prom
281
296
  * - each: List rendering with reactive updates
282
297
  */
283
298
 
299
+ /**
300
+ * Local state store interface for component-level state
301
+ */
302
+ interface LocalStateStore {
303
+ get(name: string): unknown;
304
+ set(name: string, value: unknown): void;
305
+ signals: Record<string, Signal<unknown>>;
306
+ }
284
307
  interface RenderContext {
285
308
  state: StateStore;
286
309
  actions: Record<string, CompiledAction>;
@@ -289,6 +312,10 @@ interface RenderContext {
289
312
  cleanups?: (() => void)[];
290
313
  refs?: Record<string, Element>;
291
314
  inSvg?: boolean;
315
+ localState?: {
316
+ store: LocalStateStore;
317
+ actions: Record<string, CompiledLocalAction>;
318
+ };
292
319
  }
293
320
  declare function render(node: CompiledNode, ctx: RenderContext): Node;
294
321
 
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)}`);
@@ -590,38 +602,58 @@ function createEvalContext(ctx) {
590
602
  };
591
603
  }
592
604
  async function executeAction(action, ctx) {
605
+ const extAction = action;
606
+ const isLocal = extAction._isLocalAction && extAction._localStore;
607
+ const localStore = extAction._localStore;
608
+ const delayPromises = [];
593
609
  for (const step of action.steps) {
594
610
  if (step.do === "set" || step.do === "update" || step.do === "setPath") {
595
- executeStepSync(step, ctx);
611
+ executeStepSync(step, ctx, isLocal ? localStore : void 0);
596
612
  } else if (step.do === "if") {
597
- await executeIfStep(step, ctx);
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);
598
619
  } else {
599
620
  await executeStep(step, ctx);
600
621
  }
601
622
  }
623
+ if (delayPromises.length > 0) {
624
+ await Promise.all(delayPromises);
625
+ }
602
626
  }
603
- function executeStepSync(step, ctx) {
627
+ function executeStepSync(step, ctx, localStore) {
604
628
  switch (step.do) {
605
629
  case "set":
606
- executeSetStepSync(step.target, step.value, ctx);
630
+ if (localStore) {
631
+ executeLocalSetStepSync(step.target, step.value, ctx, localStore);
632
+ } else {
633
+ executeSetStepSync(step.target, step.value, ctx);
634
+ }
607
635
  break;
608
636
  case "update":
609
- executeUpdateStepSync(step, ctx);
637
+ if (localStore) {
638
+ executeLocalUpdateStepSync(step, ctx, localStore);
639
+ } else {
640
+ executeUpdateStepSync(step, ctx);
641
+ }
610
642
  break;
611
643
  case "setPath":
612
644
  executeSetPathStepSync(step, ctx);
613
645
  break;
614
646
  }
615
647
  }
616
- async function executeIfStep(step, ctx) {
648
+ async function executeIfStep(step, ctx, localStore) {
617
649
  const evalCtx = createEvalContext(ctx);
618
650
  const condition = evaluate(step.condition, evalCtx);
619
651
  const stepsToExecute = condition ? step.then : step.else || [];
620
652
  for (const nestedStep of stepsToExecute) {
621
653
  if (nestedStep.do === "set" || nestedStep.do === "update" || nestedStep.do === "setPath") {
622
- executeStepSync(nestedStep, ctx);
654
+ executeStepSync(nestedStep, ctx, localStore);
623
655
  } else if (nestedStep.do === "if") {
624
- await executeIfStep(nestedStep, ctx);
656
+ await executeIfStep(nestedStep, ctx, localStore);
625
657
  } else {
626
658
  await executeStep(nestedStep, ctx);
627
659
  }
@@ -718,6 +750,97 @@ function executeUpdateStepSync(step, ctx) {
718
750
  }
719
751
  }
720
752
  }
753
+ function executeLocalSetStepSync(target, value, ctx, localStore) {
754
+ const evalCtx = createEvalContext(ctx);
755
+ const newValue = evaluate(value, evalCtx);
756
+ localStore.set(target, newValue);
757
+ }
758
+ function executeLocalUpdateStepSync(step, ctx, localStore) {
759
+ const { target, operation, value } = step;
760
+ const evalCtx = createEvalContext(ctx);
761
+ const currentValue = localStore.get(target);
762
+ switch (operation) {
763
+ case "toggle": {
764
+ const current = typeof currentValue === "boolean" ? currentValue : false;
765
+ localStore.set(target, !current);
766
+ break;
767
+ }
768
+ case "increment": {
769
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
770
+ const amount = typeof evalResult === "number" ? evalResult : 1;
771
+ const current = typeof currentValue === "number" ? currentValue : 0;
772
+ localStore.set(target, current + amount);
773
+ break;
774
+ }
775
+ case "decrement": {
776
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
777
+ const amount = typeof evalResult === "number" ? evalResult : 1;
778
+ const current = typeof currentValue === "number" ? currentValue : 0;
779
+ localStore.set(target, current - amount);
780
+ break;
781
+ }
782
+ case "push": {
783
+ const item = value ? evaluate(value, evalCtx) : void 0;
784
+ const arr = Array.isArray(currentValue) ? currentValue : [];
785
+ localStore.set(target, [...arr, item]);
786
+ break;
787
+ }
788
+ case "pop": {
789
+ const arr = Array.isArray(currentValue) ? currentValue : [];
790
+ localStore.set(target, arr.slice(0, -1));
791
+ break;
792
+ }
793
+ case "remove": {
794
+ const removeValue = value ? evaluate(value, evalCtx) : void 0;
795
+ const arr = Array.isArray(currentValue) ? currentValue : [];
796
+ if (typeof removeValue === "number") {
797
+ localStore.set(target, arr.filter((_2, i) => i !== removeValue));
798
+ } else {
799
+ localStore.set(target, arr.filter((x2) => x2 !== removeValue));
800
+ }
801
+ break;
802
+ }
803
+ case "merge": {
804
+ const evalResult = value ? evaluate(value, evalCtx) : {};
805
+ const mergeValue = typeof evalResult === "object" && evalResult !== null ? evalResult : {};
806
+ const current = typeof currentValue === "object" && currentValue !== null ? currentValue : {};
807
+ localStore.set(target, { ...current, ...mergeValue });
808
+ break;
809
+ }
810
+ case "replaceAt": {
811
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
812
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
813
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
814
+ if (typeof idx === "number" && idx >= 0 && idx < arr.length) {
815
+ arr[idx] = newValue;
816
+ }
817
+ localStore.set(target, arr);
818
+ break;
819
+ }
820
+ case "insertAt": {
821
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
822
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
823
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
824
+ if (typeof idx === "number" && idx >= 0) {
825
+ arr.splice(idx, 0, newValue);
826
+ }
827
+ localStore.set(target, arr);
828
+ break;
829
+ }
830
+ case "splice": {
831
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
832
+ const delCount = step.deleteCount ? evaluate(step.deleteCount, evalCtx) : 0;
833
+ const items = value ? evaluate(value, evalCtx) : [];
834
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
835
+ if (typeof idx === "number" && typeof delCount === "number") {
836
+ const insertItems = Array.isArray(items) ? items : [];
837
+ arr.splice(idx, delCount, ...insertItems);
838
+ }
839
+ localStore.set(target, arr);
840
+ break;
841
+ }
842
+ }
843
+ }
721
844
  function executeSetPathStepSync(step, ctx) {
722
845
  const evalCtx = createEvalContext(ctx);
723
846
  const pathValue = evaluate(step.path, evalCtx);
@@ -790,6 +913,18 @@ async function executeStep(step, ctx) {
790
913
  case "close":
791
914
  await executeCloseStep(step, ctx);
792
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;
793
928
  }
794
929
  }
795
930
  async function executeSetStep(target, value, ctx) {
@@ -1156,6 +1291,137 @@ async function executeCloseStep(step, ctx) {
1156
1291
  if (!ctx.connections) return;
1157
1292
  ctx.connections.close(step.connection);
1158
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
+ }
1159
1425
 
1160
1426
  // ../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js
1161
1427
  function L() {
@@ -2589,14 +2855,14 @@ function createDOMPurify() {
2589
2855
  DocumentFragment,
2590
2856
  HTMLTemplateElement,
2591
2857
  Node: Node2,
2592
- Element,
2858
+ Element: Element2,
2593
2859
  NodeFilter,
2594
2860
  NamedNodeMap = window2.NamedNodeMap || window2.MozNamedAttrMap,
2595
2861
  HTMLFormElement,
2596
2862
  DOMParser,
2597
2863
  trustedTypes
2598
2864
  } = window2;
2599
- const ElementPrototype = Element.prototype;
2865
+ const ElementPrototype = Element2.prototype;
2600
2866
  const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
2601
2867
  const remove = lookupGetter(ElementPrototype, "remove");
2602
2868
  const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
@@ -3050,7 +3316,7 @@ function createDOMPurify() {
3050
3316
  _forceRemove(currentNode);
3051
3317
  return true;
3052
3318
  }
3053
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
3319
+ if (currentNode instanceof Element2 && !_checkValidNamespace(currentNode)) {
3054
3320
  _forceRemove(currentNode);
3055
3321
  return true;
3056
3322
  }
@@ -13146,6 +13412,188 @@ function isSvgTag(tag) {
13146
13412
  function isEventHandler(value) {
13147
13413
  return typeof value === "object" && value !== null && "event" in value && "action" in value;
13148
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
+ }
13149
13597
  function render(node, ctx) {
13150
13598
  switch (node.kind) {
13151
13599
  case "element":
@@ -13160,6 +13608,10 @@ function render(node, ctx) {
13160
13608
  return renderMarkdown(node, ctx);
13161
13609
  case "code":
13162
13610
  return renderCode(node, ctx);
13611
+ case "portal":
13612
+ return renderPortal(node, ctx);
13613
+ case "localState":
13614
+ return renderLocalState(node, ctx);
13163
13615
  default:
13164
13616
  throw new Error("Unknown node kind");
13165
13617
  }
@@ -13180,34 +13632,13 @@ function renderElement(node, ctx) {
13180
13632
  if (isEventHandler(propValue)) {
13181
13633
  const handler = propValue;
13182
13634
  const eventName = handler.event;
13183
- el.addEventListener(eventName, async (event) => {
13184
- const action = ctx.actions[handler.action];
13185
- if (action) {
13186
- const eventLocals = {};
13187
- const target = event.target;
13188
- if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
13189
- eventLocals["value"] = target.value;
13190
- if (target instanceof HTMLInputElement && target.type === "checkbox") {
13191
- eventLocals["checked"] = target.checked;
13192
- }
13193
- }
13194
- let payload = void 0;
13195
- if (handler.payload) {
13196
- payload = evaluatePayload(handler.payload, {
13197
- state: ctx.state,
13198
- locals: { ...ctx.locals, ...eventLocals },
13199
- ...ctx.imports && { imports: ctx.imports }
13200
- });
13201
- }
13202
- const actionCtx = {
13203
- state: ctx.state,
13204
- actions: ctx.actions,
13205
- locals: { ...ctx.locals, ...eventLocals, payload },
13206
- eventPayload: payload
13207
- };
13208
- await executeAction(action, actionCtx);
13209
- }
13210
- });
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
+ }
13211
13642
  } else {
13212
13643
  const cleanup = createEffect(() => {
13213
13644
  const value = evaluate(propValue, { state: ctx.state, locals: ctx.locals, ...ctx.imports && { imports: ctx.imports } });
@@ -13547,6 +13978,135 @@ function renderCode(node, ctx) {
13547
13978
  ctx.cleanups?.push(cleanup);
13548
13979
  return container;
13549
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
+ }
14016
+ function createLocalStateStore(stateDefs) {
14017
+ const signals = {};
14018
+ for (const [name, def] of Object.entries(stateDefs)) {
14019
+ signals[name] = createSignal(def.initial);
14020
+ }
14021
+ return {
14022
+ get(name) {
14023
+ return signals[name]?.get();
14024
+ },
14025
+ set(name, value) {
14026
+ signals[name]?.set(value);
14027
+ },
14028
+ signals
14029
+ };
14030
+ }
14031
+ function createLocalsWithLocalState(baseLocals, localStore) {
14032
+ return new Proxy(baseLocals, {
14033
+ get(target, prop) {
14034
+ if (prop in localStore.signals) {
14035
+ return localStore.get(prop);
14036
+ }
14037
+ return target[prop];
14038
+ },
14039
+ has(target, prop) {
14040
+ if (prop in localStore.signals) return true;
14041
+ return prop in target;
14042
+ },
14043
+ ownKeys(target) {
14044
+ const keys = Reflect.ownKeys(target);
14045
+ for (const key2 of Object.keys(localStore.signals)) {
14046
+ if (!keys.includes(key2)) keys.push(key2);
14047
+ }
14048
+ return keys;
14049
+ },
14050
+ getOwnPropertyDescriptor(target, prop) {
14051
+ if (prop in localStore.signals) {
14052
+ return { enumerable: true, configurable: true };
14053
+ }
14054
+ return Reflect.getOwnPropertyDescriptor(target, prop);
14055
+ }
14056
+ });
14057
+ }
14058
+ function createStateWithLocalState(globalState, localStore) {
14059
+ return {
14060
+ get(name) {
14061
+ if (name in localStore.signals) {
14062
+ return localStore.get(name);
14063
+ }
14064
+ return globalState.get(name);
14065
+ },
14066
+ set(name, value) {
14067
+ globalState.set(name, value);
14068
+ },
14069
+ setPath(name, path, value) {
14070
+ globalState.setPath(name, path, value);
14071
+ },
14072
+ subscribe(name, fn) {
14073
+ if (name in localStore.signals) {
14074
+ return localStore.signals[name].subscribe(fn);
14075
+ }
14076
+ return globalState.subscribe(name, fn);
14077
+ },
14078
+ getPath(name, path) {
14079
+ return globalState.getPath(name, path);
14080
+ },
14081
+ subscribeToPath(name, path, fn) {
14082
+ return globalState.subscribeToPath(name, path, fn);
14083
+ }
14084
+ };
14085
+ }
14086
+ function renderLocalState(node, ctx) {
14087
+ const localStore = createLocalStateStore(node.state);
14088
+ const mergedLocals = createLocalsWithLocalState(ctx.locals, localStore);
14089
+ const mergedState = createStateWithLocalState(ctx.state, localStore);
14090
+ const mergedActions = { ...ctx.actions };
14091
+ for (const [name, action] of Object.entries(node.actions)) {
14092
+ mergedActions[name] = {
14093
+ ...action,
14094
+ _isLocalAction: true,
14095
+ _localStore: localStore
14096
+ };
14097
+ }
14098
+ const childCtx = {
14099
+ ...ctx,
14100
+ state: mergedState,
14101
+ locals: mergedLocals,
14102
+ actions: mergedActions,
14103
+ localState: {
14104
+ store: localStore,
14105
+ actions: node.actions
14106
+ }
14107
+ };
14108
+ return render(node.child, childCtx);
14109
+ }
13550
14110
 
13551
14111
  // src/app.ts
13552
14112
  function createApp(program, mount) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.12.2",
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.9.1",
22
- "@constela/core": "0.9.1"
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": "5.0.1"
32
+ "@constela/server": "7.0.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"