@zenithbuild/core 1.1.0 → 1.2.1

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.
@@ -57,11 +57,6 @@ function lowerNode(
57
57
  return lowerExpressionNode(node, filePath, expressions)
58
58
 
59
59
  case 'element': {
60
- // Check if this is a <for> element directive
61
- if (node.tag === 'for') {
62
- return lowerForElement(node, filePath, expressions)
63
- }
64
-
65
60
  // Check if this is an <html-content> element directive
66
61
  if (node.tag === 'html-content') {
67
62
  return lowerHtmlContentElement(node, filePath, expressions)
@@ -287,95 +282,6 @@ function lowerInlineFragment(
287
282
  return node
288
283
  }
289
284
 
290
- /**
291
- * Lower <for> element directive to LoopFragmentNode
292
- *
293
- * Syntax: <for each="item" in="items">...body...</for>
294
- * Or: <for each="item, index" in="items">...body...</for>
295
- *
296
- * This is compile-time sugar for {items.map(item => ...)}
297
- */
298
- function lowerForElement(
299
- node: import('../ir/types').ElementNode,
300
- filePath: string,
301
- expressions: ExpressionIR[]
302
- ): LoopFragmentNode {
303
- // Extract 'each' and 'in' attributes
304
- const eachAttr = node.attributes.find(a => a.name === 'each')
305
- const inAttr = node.attributes.find(a => a.name === 'in')
306
-
307
- if (!eachAttr || typeof eachAttr.value !== 'string') {
308
- throw new InvariantError(
309
- 'ZEN001',
310
- `<for> element requires an 'each' attribute specifying the item variable`,
311
- 'Usage: <for each="item" in="items">...body...</for>',
312
- filePath,
313
- node.location.line,
314
- node.location.column
315
- )
316
- }
317
-
318
- if (!inAttr || typeof inAttr.value !== 'string') {
319
- throw new InvariantError(
320
- 'ZEN001',
321
- `<for> element requires an 'in' attribute specifying the source array`,
322
- 'Usage: <for each="item" in="items">...body...</for>',
323
- filePath,
324
- node.location.line,
325
- node.location.column
326
- )
327
- }
328
-
329
- // Parse item variable (may include index: "item, index" or "item, i")
330
- const eachValue = eachAttr.value.trim()
331
- let itemVar: string
332
- let indexVar: string | undefined
333
-
334
- if (eachValue.includes(',')) {
335
- const parts = eachValue.split(',').map(p => p.trim())
336
- itemVar = parts[0]!
337
- indexVar = parts[1]
338
- } else {
339
- itemVar = eachValue
340
- }
341
-
342
- const source = inAttr.value.trim()
343
-
344
- // Create loop context for the body
345
- const loopVariables = [itemVar]
346
- if (indexVar) {
347
- loopVariables.push(indexVar)
348
- }
349
-
350
- const bodyLoopContext: LoopContext = {
351
- variables: node.loopContext
352
- ? [...node.loopContext.variables, ...loopVariables]
353
- : loopVariables,
354
- mapSource: source
355
- }
356
-
357
- // Lower children with loop context
358
- const body = node.children.map(child => {
359
- // Recursively lower children
360
- const lowered = lowerNode(child, filePath, expressions)
361
- // Attach loop context to children that need it
362
- if ('loopContext' in lowered) {
363
- return { ...lowered, loopContext: bodyLoopContext }
364
- }
365
- return lowered
366
- })
367
-
368
- return {
369
- type: 'loop-fragment',
370
- source,
371
- itemVar,
372
- indexVar,
373
- body,
374
- location: node.location,
375
- loopContext: bodyLoopContext
376
- }
377
- }
378
-
379
285
  /**
380
286
  * Lower <html-content> element directive
381
287
  *
@@ -126,7 +126,8 @@ export function transformNode(
126
126
  }
127
127
 
128
128
  case 'loop-fragment': {
129
- // Loop fragment: {items.map(item => <li>...</li>)} or <for each="item" in="items">
129
+ // Loop fragment: {items.map(item => <li>...</li>)}
130
+ // .map() is compile-time sugar, lowered to LoopFragmentNode
130
131
  // For SSR/SSG, we render one instance of the body as a template
131
132
  // The runtime will hydrate and expand this for each actual item
132
133
  const loopNode = node as LoopFragmentNode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/core",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Core library for the Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -77,4 +77,4 @@
77
77
  "parse5": "^8.0.0",
78
78
  "picocolors": "^1.1.1"
79
79
  }
80
- }
80
+ }
@@ -309,80 +309,461 @@ export function getExpression(id: string): ((state: any) => any) | undefined {
309
309
  // Hydration Functions
310
310
  // ============================================
311
311
 
312
- const bindings: Array<{ node: Element; type: string; expressionId: string; attributeName?: string }> = [];
312
+ // ============================================
313
+ // Hydration & Binding System
314
+ // ============================================
313
315
 
314
- export function hydrate(state: any, container?: Element | Document): void {
315
- const root = container || document;
316
+ interface BaseBinding {
317
+ node: Node;
318
+ type: 'text' | 'attribute' | 'html' | 'conditional' | 'optional' | 'loop';
319
+ id: string; // Binding/Expression ID
320
+ }
316
321
 
317
- // Clear existing bindings
318
- bindings.length = 0;
322
+ interface TextBinding extends BaseBinding {
323
+ type: 'text';
324
+ node: Text;
325
+ }
319
326
 
320
- // Find all text expression placeholders
321
- const textPlaceholders = root.querySelectorAll('[data-zen-text]');
322
- textPlaceholders.forEach((node) => {
323
- const expressionId = node.getAttribute('data-zen-text');
324
- if (!expressionId) return;
327
+ interface AttributeBinding extends BaseBinding {
328
+ type: 'attribute';
329
+ node: Element;
330
+ attrName: string;
331
+ }
325
332
 
326
- bindings.push({ node: node as Element, type: 'text', expressionId });
327
- updateTextBinding(node as Element, expressionId, state);
328
- });
333
+ interface HtmlBinding extends BaseBinding {
334
+ type: 'html';
335
+ node: Element;
336
+ }
329
337
 
330
- // Find attribute bindings
331
- const attrSelectors = ['class', 'style', 'src', 'href', 'disabled', 'checked'];
332
- for (const attr of attrSelectors) {
333
- const attrPlaceholders = root.querySelectorAll(`[data-zen-attr-${attr}]`);
334
- attrPlaceholders.forEach((node) => {
335
- const expressionId = node.getAttribute(`data-zen-attr-${attr}`);
336
- if (!expressionId) return;
338
+ interface ConditionalBinding extends BaseBinding {
339
+ type: 'conditional';
340
+ node: Element; // The container div with data-zen-cond
341
+ trueBranch?: Node[]; // Cached true branch nodes
342
+ falseBranch?: Node[]; // Cached false branch nodes
343
+ currentBranch: boolean | null; // true, false, or null (uninitialized)
344
+ }
337
345
 
338
- bindings.push({ node: node as Element, type: 'attribute', expressionId, attributeName: attr });
339
- updateAttributeBinding(node as Element, attr, expressionId, state);
340
- });
346
+ interface OptionalBinding extends BaseBinding {
347
+ type: 'optional';
348
+ node: Element; // The container div with data-zen-opt
349
+ content?: Node[]; // Cached content nodes
350
+ isVisible: boolean | null;
351
+ }
352
+
353
+ interface LoopBinding extends BaseBinding {
354
+ type: 'loop';
355
+ node: Element; // The container div with data-zen-loop
356
+ template: Node[]; // THe template nodes (cloned from initial content)
357
+ items: any[]; // Current items list
358
+ itemBindings: Binding[][]; // Bindings for each item
359
+ itemVar: string;
360
+ indexVar: string | null;
361
+ sourceExpr: string;
362
+ }
363
+
364
+ type Binding = TextBinding | AttributeBinding | HtmlBinding | ConditionalBinding | OptionalBinding | LoopBinding;
365
+
366
+ // Root bindings for the application
367
+ const rootBindings: Binding[] = [];
368
+
369
+ /**
370
+ * Recursive Hydration (Tree Walker)
371
+ *
372
+ * Scans the DOM tree for bindings, respecting scope boundaries.
373
+ * Returns a list of bindings found in this subtree (excluding those inside child scopes like loops).
374
+ */
375
+
376
+ /**
377
+ * Manual recursive traversal to better handle skipping subtrees
378
+ */
379
+ function scanBindings(node: Node, bindings: Binding[]): void {
380
+ if (node.nodeType === Node.ELEMENT_NODE) {
381
+ const element = node as Element;
382
+
383
+ // Check for Loop - Stop traversal into children if found
384
+ if (element.hasAttribute('data-zen-loop')) {
385
+ const id = element.getAttribute('data-zen-loop')!;
386
+ const source = element.getAttribute('data-zen-source')!;
387
+ const itemVar = element.getAttribute('data-zen-item')!;
388
+ const indexVar = element.getAttribute('data-zen-index');
389
+
390
+ // Capture template from initial content (SSR)
391
+ // Note: For client-side nav, this might modify `element` immediately?
392
+ // For now assume standard hydration of SSR content
393
+ const template = Array.from(element.childNodes).map(n => n.cloneNode(true));
394
+
395
+ // The loop binding itself manages the children.
396
+ // We do NOT scan children here. The loop update() will scan/hydrate instances.
397
+
398
+ // Clear initial SSR content so we can re-render fresh?
399
+ // Or try to hydrate existing? Hydrating lists is hard (keys etc).
400
+ // Simplest safe fix: Clear and re-render.
401
+ element.innerHTML = '';
402
+
403
+ bindings.push({
404
+ type: 'loop',
405
+ node: element,
406
+ id,
407
+ sourceExpr: source,
408
+ itemVar,
409
+ indexVar,
410
+ template,
411
+ items: [],
412
+ itemBindings: []
413
+ } as LoopBinding);
414
+
415
+ return; // STOP recursion into loop children
416
+ }
417
+
418
+ // Check for Conditional - Stop recursion?
419
+ // Actually, conditional blocks contain children we WANT to bind if they are currently visible.
420
+ // But if we toggle, we need to re-scan.
421
+ // So ConditionalBinding should manage its children.
422
+ if (element.hasAttribute('data-zen-cond')) {
423
+ const id = element.getAttribute('data-zen-cond')!;
424
+ // We assume the true/false branches are initially present as separate containers
425
+ // OR `data-zen-cond-true` / `data-zen-cond-false` markers?
426
+ // The compiler output:
427
+ // <div data_zen_cond="id" data_zen_cond_true style="display:contents">...</div>
428
+ // <div data_zen_cond="id" data_zen_cond_false style="display:none">...</div>
429
+ // These are siblings usually? Or nested?
430
+ // Wait, compiler generates TWO divs.
431
+ // <div data-zen-cond="id" ...true> and <div data-zen-cond="id" ...false>
432
+ // Each is a separate binding effectively?
433
+ // If they share the same ID, they are part of the same logic.
434
+ // Scanning will find both. We can treat them as independent toggle-able areas?
435
+
436
+ // Actually, simpler to treat them as ConditionalBinding.
437
+ // But we need to know which branch it is.
438
+ const isTrueBranch = element.hasAttribute('data-zen-cond-true');
439
+
440
+ // For now, let's treat them as distinct bindings that listen to the same expression
441
+ // and toggle visibility.
442
+ // We DO want to scan their children because they might be visible.
443
+
444
+ // Optimization: If display:none, maybe don't scan yet?
445
+ // But we want to be ready to show.
446
+
447
+ // Let's implement ConditionalBinding to manage visibility AND recurse.
448
+ bindings.push({
449
+ type: 'conditional',
450
+ node: element,
451
+ id,
452
+ currentBranch: null, // Force update
453
+
454
+ } as any); // Simplification: Just toggle display
455
+ // NO, wait. If we hide it, we shouldn't update its children's bindings if they depend on ephemeral state?
456
+ // Actually, simplest is just toggle binding.
457
+ // Let's rely on standard attribute binding?
458
+ // No, standard attribute binding doesn't handle children.
459
+
460
+ // Revisiting the compiler:
461
+ // It's just a div with an ID. The logic is "If cond is true, show TrueDiv, hide FalseDiv".
462
+ // So we can have a Binding that just toggles 'style.display'.
463
+ // And we CONTINUE scanning children.
464
+ }
465
+
466
+ // Optional? Same.
467
+
468
+ // Bind attributes
469
+ const attrSelectors = ['class', 'style', 'src', 'href', 'disabled', 'checked', 'value', 'placeholder'];
470
+ for (const attr of attrSelectors) {
471
+ const attrKey = `data-zen-attr-${attr}`;
472
+ if (element.hasAttribute(attrKey)) {
473
+ bindings.push({
474
+ type: 'attribute',
475
+ node: element,
476
+ id: element.getAttribute(attrKey)!,
477
+ attrName: attr
478
+ });
479
+ }
480
+ }
481
+
482
+ // Bind HTML
483
+ if (element.hasAttribute('data-zen-html')) {
484
+ bindings.push({
485
+ type: 'html',
486
+ node: element,
487
+ id: element.getAttribute('data-zen-html')!
488
+ });
489
+ }
490
+
491
+ // Bind Events
492
+ bindEventsHelpers(element);
341
493
  }
342
494
 
343
- // Bind event handlers
344
- bindEvents(root);
495
+ // Check Text Nodes
496
+ if (node.nodeType === Node.TEXT_NODE) {
497
+ // Text nodes can't have attributes, so we look at parent?
498
+ // No, Zenith compiler puts `data-zen-text="id"` on the PARENT element usually?
499
+ // Or strictly on the element wrapping the text?
500
+ // Compiler: <span data-zen-text="id"></span> or similar.
501
+ // Wait, `transformNode` for text binding:
502
+ // Returns `<!--binding:id-->`? No.
503
+ // It returns a TextNode in IR. `generateBindings` validates "Text binding must have target 'data-zen-text'".
504
+ // `transformNode`: if binding exists, it sets `data-zen-text` attribute on the element?
505
+ // Wait, text nodes don't have attributes.
506
+ // The compiler wraps text in a generic element or expects the parent?
507
+ // Usually framework puts it on the parent element.
508
+ // Let's check `parseTemplate.ts` or `transformNode.ts`...
509
+ // Assuming the element with `data-zen-text` owns the text content.
510
+ }
345
511
 
346
- // Trigger mount
347
- triggerMount();
512
+ // Check parent for Text Binding (on Element)
513
+ if (node.nodeType === Node.ELEMENT_NODE) {
514
+ const el = node as Element;
515
+ if (el.hasAttribute('data-zen-text')) {
516
+ bindings.push({
517
+ type: 'text',
518
+ node: el.firstChild as Text || el.appendChild(document.createTextNode('')), // Ensure text node exists
519
+ id: el.getAttribute('data-zen-text')!
520
+ } as TextBinding);
521
+ // We don't skip children here, but usually text binding replaces content.
522
+ }
523
+ }
524
+
525
+ // Recurse to children (unless stopped above)
526
+ let child = node.firstChild;
527
+ while (child) {
528
+ scanBindings(child, bindings);
529
+ child = child.nextSibling;
530
+ }
348
531
  }
349
532
 
350
- function updateTextBinding(node: Element, expressionId: string, state: any): void {
351
- const expression = expressionRegistry.get(expressionId);
352
- if (!expression) {
353
- console.warn(`[Zenith] Expression ${expressionId} not found`);
354
- return;
533
+ function bindEventsHelpers(element: Element) {
534
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
535
+ for (const eventType of eventTypes) {
536
+ const attr = `data-zen-${eventType}`;
537
+ if (element.hasAttribute(attr)) {
538
+ // We attach handler immediately?
539
+ // Handler needs to access runtime state.
540
+ // We'll use a closure that calls the global/scoped handler at runtime.
541
+ const handlerName = element.getAttribute(attr)!;
542
+
543
+ // Remove old
544
+ const oldHandler = (element as any)[`__zen_${eventType}`];
545
+ if (oldHandler) element.removeEventListener(eventType, oldHandler);
546
+
547
+ // Add new
548
+ const handler = (e: Event) => dispatchEvent(e, handlerName, element);
549
+ (element as any)[`__zen_${eventType}`] = handler;
550
+ element.addEventListener(eventType, handler);
551
+ }
552
+ }
553
+ }
554
+
555
+ function dispatchEvent(event: Event, handlerName: string, element: Element) {
556
+ // We need to find the SCOPE.
557
+ // Event handler might be looking for `item` (loop variable).
558
+ // This is tricky with standard DOM events.
559
+ // We attach scope info to the DOM element?
560
+ // When LoopBinding creates nodes, it should attach `_zen_scope` property to the root of the item?
561
+
562
+ // Finding scope: traverse up to find an element with `_zen_scope`.
563
+ let target = element as any;
564
+ let scope = {};
565
+
566
+ while (target) {
567
+ if (target._zen_scope) {
568
+ scope = { ...target._zen_scope, ...scope }; // Merge scopes (inner wins? No, inner is more specific)
569
+ // Actually, prototype chain or Object.assign?
570
+ // List items usually have unique props.
571
+ // Let's accumulate.
572
+ // For a simple hierarchy: Global < LoopItem.
573
+ // We start matching from Global? No.
574
+ // The handler execution needs the collected scope.
575
+ break; // Assuming flat scope per loop item for now or merge?
576
+ }
577
+ target = target.parentNode;
355
578
  }
356
579
 
580
+ // Global scopes
581
+ // We don't easily have access to the *current* global state passed to update() here,
582
+ // unless we store it globally.
583
+ // `expressionRegistry` handlers take `state`.
584
+ // We need to fetch the latest state?
585
+ // Zenith signals usually read current value directly.
586
+ // But `expression(state)` pattern implies pure function of state.
587
+
588
+ // For events: usually they call a function. `increment()`.
589
+ // That function access signals/state directly.
590
+ // BUT if it uses arguments like `doSomething(item)`, `item` comes from scope.
591
+
592
+ // Only expressions registered in registry need `state` passed?
593
+ // If `handlerName` is in `__ZENITH_EXPRESSIONS__`, calling it requires state.
594
+ // Which state? One with loop vars.
595
+
596
+ // HACK: for now, we rely on the fact that most handlers are simple fn calls.
597
+ // If they are expressions expecting args, we assume they are bound or don't need args?
598
+ // Wait, `<button onclick="remove(item)">`.
599
+ // Compiler lower this to an expression: `(state) => remove(state.item)`.
600
+ // So we DO need to pass the scope-enriched state to the handler if it's an expression.
601
+
602
+ const handlerFunc = (window as any)[handlerName] || (window as any).__ZENITH_EXPRESSIONS__?.get(handlerName);
603
+
604
+ if (typeof handlerFunc === 'function') {
605
+ // Determine effective state
606
+ // We might need a global `getLastState()` or similar if strictly functional.
607
+ // Or we pass `scope` (which has item) as the state?
608
+ // If generic state is required, we are in trouble without a global reference.
609
+ // Let's assume `window.__zenith_last_state` is available or similar?
610
+ // Added `window.__zenith_currentState` in update().
611
+
612
+ const globalState = (window as any).__zenith_currentState || {};
613
+ const effectiveState = { ...globalState, ...scope };
614
+
615
+ handlerFunc(event, element, effectiveState); // Pass state as 3rd arg? Or 1st?
616
+ // Expression signature: (state) => ...
617
+ // Event handler signature: (e, el) => ...
618
+ // Conflict.
619
+ // Zenith compiler usually generates `(state) => (e) => ...` or similar for events?
620
+ // Or the expression IS the handler body?
621
+ // If it's `(state) => ...`, we call it to get the result.
622
+ // If result is function, call it with event?
623
+
624
+ // Let's try calling with state. If it returns function, call that.
625
+ try {
626
+ const result = handlerFunc(effectiveState);
627
+ if (typeof result === 'function') {
628
+ result(event, element);
629
+ }
630
+ } catch (e) {
631
+ // It might be a direct event handler (e) => ...
632
+ handlerFunc(event, element);
633
+ }
634
+ }
635
+ }
636
+
637
+ export function hydrate(state: any, container?: Element | Document): void {
638
+ const root = container || document;
639
+ // Clear global bindings
640
+ rootBindings.length = 0;
641
+
642
+ // Store initial state for events
643
+ (window as any).__zenith_currentState = state;
644
+
645
+ scanBindings(root, rootBindings);
646
+
647
+ // Initial update
648
+ updateBindings(rootBindings, state);
649
+ triggerMount();
650
+ }
651
+
652
+ /**
653
+ * Update Loop Logic
654
+ */
655
+ function updateLoop(binding: LoopBinding, state: any) {
656
+ const { node, id, sourceExpr, itemVar, indexVar, template } = binding;
657
+
658
+ // 1. Evaluate Source
659
+ const expr = expressionRegistry.get(sourceExpr);
660
+ if (!expr) return;
661
+
662
+ let list = [];
357
663
  try {
358
- const result = expression(state);
359
- if (result === null || result === undefined || result === false) {
360
- node.textContent = '';
361
- } else if (typeof result === 'string') {
362
- if (result.trim().startsWith('<') && result.trim().endsWith('>')) {
363
- node.innerHTML = result;
364
- } else {
365
- node.textContent = result;
664
+ list = expr(state) || [];
665
+ } catch (e) { console.error('Loop source error', e); }
666
+
667
+ if (!Array.isArray(list)) list = [];
668
+
669
+ // 2. Reconcile (Naive: Clear and Re-render)
670
+ // Optimization: Reuse existing items?
671
+ // For Patch 1.1.0, let's keep it simple and safe: Full Re-render.
672
+ // Ideally we diff.
673
+
674
+ // Detect if nothing changed? (Deep equals or ref check)
675
+ if (binding.items === list) {
676
+ // Same reference, assumes no change?
677
+ // In mutable state (Proxy), the array might be same ref but mutated.
678
+ // We probably should re-render or at least re-update children.
679
+ // Let's fall through.
680
+ }
681
+ binding.items = list;
682
+
683
+ node.innerHTML = '';
684
+ binding.itemBindings = []; // Clear child bindings
685
+
686
+ list.forEach((item, index) => {
687
+ // Create Scope
688
+ const scope = { [itemVar]: item };
689
+ if (indexVar) scope[indexVar] = index;
690
+
691
+ // Clone Template
692
+ const fragment = document.createDocumentFragment();
693
+ template.forEach(n => fragment.appendChild(n.cloneNode(true)));
694
+
695
+ // Mark Scope on Root Elements of Item
696
+ // (Used for event delegation)
697
+ Array.from(fragment.childNodes).forEach((child: any) => {
698
+ if (child.nodeType === 1) child._zen_scope = scope;
699
+ });
700
+
701
+ // Hydrate this instance (collect bindings)
702
+ const instanceBindings: Binding[] = [];
703
+ // Scan fragment BEFORE appending? Or after?
704
+ // Appending first is easier for traversal, but we want to bind to the specific nodes.
705
+ scanBindings(fragment, instanceBindings);
706
+ binding.itemBindings.push(instanceBindings);
707
+
708
+ node.appendChild(fragment);
709
+
710
+ // Initial Update for this instance
711
+ updateBindings(instanceBindings, { ...state, ...scope });
712
+ });
713
+ }
714
+
715
+ function updateBindings(bindingsList: Binding[], state: any) {
716
+ for (const binding of bindingsList) {
717
+ if (binding.type === 'text') {
718
+ const expr = expressionRegistry.get(binding.id);
719
+ if (expr) {
720
+ try { binding.node.textContent = String(expr(state) ?? ''); }
721
+ catch (e) { }
366
722
  }
367
- } else if (result instanceof Node) {
368
- node.innerHTML = '';
369
- node.appendChild(result);
370
- } else if (Array.isArray(result)) {
371
- node.innerHTML = '';
372
- const fragment = document.createDocumentFragment();
373
- result.flat(Infinity).forEach(item => {
374
- if (item instanceof Node) fragment.appendChild(item);
375
- else if (item != null && item !== false) fragment.appendChild(document.createTextNode(String(item)));
376
- });
377
- node.appendChild(fragment);
378
- } else {
379
- node.textContent = String(result);
723
+ } else if (binding.type === 'attribute') {
724
+ updateAttributeBinding(binding.node, binding.attrName, binding.id, state);
725
+ } else if (binding.type === 'html') {
726
+ const expr = expressionRegistry.get(binding.id);
727
+ if (expr) {
728
+ try {
729
+ const val = expr(state);
730
+ binding.node.innerHTML = String(val ?? '');
731
+ } catch (e) { }
732
+ }
733
+ } else if (binding.type === 'conditional' || binding.type === 'optional') {
734
+ // Toggle display based on truthiness
735
+ const expr = expressionRegistry.get(binding.id);
736
+ if (expr) {
737
+ const val = !!expr(state);
738
+ // Check if inverted (data-zen-cond-false)
739
+ const isInverse = (binding.node as Element).hasAttribute('data-zen-cond-false');
740
+ const shouldShow = isInverse ? !val : val;
741
+
742
+ (binding.node as HTMLElement).style.display = shouldShow ? 'contents' : 'none';
743
+ }
744
+ } else if (binding.type === 'loop') {
745
+ updateLoop(binding as LoopBinding, state);
380
746
  }
381
- } catch (error) {
382
- console.error(`[Zenith] Error evaluating expression ${expressionId}:`, error);
383
747
  }
384
748
  }
385
749
 
750
+ export function update(state: any): void {
751
+ (window as any).__zenith_currentState = state;
752
+ updateBindings(rootBindings, state);
753
+ }
754
+
755
+ export function cleanup(container?: Element | Document): void {
756
+ rootBindings.length = 0;
757
+ rootBindings.length = 0;
758
+ triggerUnmount();
759
+ }
760
+
761
+ // ... existing helper functions (updateAttributeBinding, bindEvents) need slight adjustments or imports?
762
+ // We need to keep updateAttributeBinding available.
763
+
764
+ // REUSING EXISTING updateAttributeBinding from previous implementation
765
+ // (Need to make sure it's defined or moved)
766
+
386
767
  function updateAttributeBinding(element: Element, attrName: string, expressionId: string, state: any): void {
387
768
  const expression = expressionRegistry.get(expressionId);
388
769
  if (!expression) return;
@@ -413,77 +794,18 @@ function updateAttributeBinding(element: Element, attrName: string, expressionId
413
794
  }
414
795
  }
415
796
 
416
- export function update(state: any): void {
417
- for (const binding of bindings) {
418
- if (binding.type === 'text') {
419
- updateTextBinding(binding.node, binding.expressionId, state);
420
- } else if (binding.type === 'attribute' && binding.attributeName) {
421
- updateAttributeBinding(binding.node, binding.attributeName, binding.expressionId, state);
422
- }
423
- }
424
- }
425
-
426
797
  export function bindEvents(container: Element | Document): void {
427
- const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
428
-
429
- for (const eventType of eventTypes) {
430
- const elements = container.querySelectorAll(`[data-zen-${eventType}]`);
431
-
432
- elements.forEach((element) => {
433
- const handlerName = element.getAttribute(`data-zen-${eventType}`);
434
- if (!handlerName) return;
435
-
436
- // Remove existing handler if any
437
- const handlerKey = `__zen_${eventType}_handler`;
438
- const existingHandler = (element as any)[handlerKey];
439
- if (existingHandler) {
440
- element.removeEventListener(eventType, existingHandler);
441
- }
442
-
443
- // Create new handler
444
- const handler = (event: Event) => {
445
- try {
446
- // Try window first, then expression registry
447
- let handlerFunc = (window as any)[handlerName];
448
- if (typeof handlerFunc !== 'function') {
449
- handlerFunc = (window as any).__ZENITH_EXPRESSIONS__?.get(handlerName);
450
- }
451
-
452
- if (typeof handlerFunc === 'function') {
453
- handlerFunc(event, element);
454
- } else {
455
- console.warn(`[Zenith] Event handler "${handlerName}" not found`);
456
- }
457
- } catch (error) {
458
- console.error(`[Zenith] Error executing handler "${handlerName}":`, error);
459
- }
460
- };
461
-
462
- (element as any)[handlerKey] = handler;
463
- element.addEventListener(eventType, handler);
464
- });
798
+ // Legacy support or external call?
799
+ // Our scanBindings() handles events.
800
+ // If called manually, we traverse?
801
+ if (container.nodeType === Node.ELEMENT_NODE) {
802
+ bindEventsHelpers(container as Element);
803
+ // And children...
804
+ const all = (container as Element).querySelectorAll('*');
805
+ all.forEach(el => bindEventsHelpers(el));
465
806
  }
466
807
  }
467
808
 
468
- export function cleanup(container?: Element | Document): void {
469
- const root = container || document;
470
- const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
471
-
472
- for (const eventType of eventTypes) {
473
- const elements = root.querySelectorAll(`[data-zen-${eventType}]`);
474
- elements.forEach((element) => {
475
- const handlerKey = `__zen_${eventType}_handler`;
476
- const handler = (element as any)[handlerKey];
477
- if (handler) {
478
- element.removeEventListener(eventType, handler);
479
- delete (element as any)[handlerKey];
480
- }
481
- });
482
- }
483
-
484
- bindings.length = 0;
485
- triggerUnmount();
486
- }
487
809
 
488
810
  // ============================================
489
811
  // Plugin Runtime Data Access