lightview 2.3.8 → 2.4.4

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.
Files changed (38) hide show
  1. package/.gemini/CODE_ANALYSIS_AND_IMPROVEMENT_PLAN.md +56 -0
  2. package/AI-GUIDANCE.md +259 -0
  3. package/README.md +35 -0
  4. package/components/data-display/diff.js +36 -4
  5. package/docs/api/hypermedia.html +75 -5
  6. package/docs/api/index.html +3 -3
  7. package/docs/api/nav.html +0 -16
  8. package/docs/articles/html-vs-json-partials.md +102 -0
  9. package/docs/articles/lightview-vs-htmx.md +610 -0
  10. package/docs/benchmarks/tagged-fragment.js +36 -0
  11. package/docs/components/chart.html +157 -210
  12. package/docs/components/component-nav.html +1 -1
  13. package/docs/components/diff.html +33 -21
  14. package/docs/components/gallery.html +107 -4
  15. package/docs/components/index.css +18 -3
  16. package/docs/components/index.html +20 -9
  17. package/docs/dom-benchmark.html +644 -0
  18. package/docs/getting-started/index.html +2 -2
  19. package/docs/hypermedia/index.html +391 -0
  20. package/docs/hypermedia/nav.html +17 -0
  21. package/docs/index.html +128 -18
  22. package/index.html +59 -10
  23. package/lightview-all.js +223 -67
  24. package/lightview-cdom.js +1 -2
  25. package/lightview-x.js +144 -13
  26. package/lightview.js +85 -277
  27. package/package.json +2 -2
  28. package/src/lightview-cdom.js +1 -5
  29. package/src/lightview-x.js +158 -27
  30. package/src/lightview.js +94 -60
  31. package/docs/articles/calculator-no-javascript-hackernoon.md +0 -283
  32. package/docs/articles/calculator-no-javascript.md +0 -290
  33. package/docs/articles/part1-reference.md +0 -236
  34. package/lightview.js.bak +0 -1
  35. package/test-xpath.html +0 -63
  36. package/test_error.txt +0 -0
  37. package/test_output.txt +0 -0
  38. package/test_output_full.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  import { signal, effect, getRegistry } from './reactivity/signal.js';
2
- import { state, getOrSet } from './reactivity/state.js';
2
+ import { state, getState, getOrSet } from './reactivity/state.js';
3
3
 
4
4
 
5
5
  /**
@@ -552,15 +552,107 @@ const insert = (elements, parent, location, markerId, { element, setupChildren }
552
552
 
553
553
  const isPath = (s) => typeof s === 'string' && !isDangerousProtocol(s) && /^(https?:|\.|\/|[\w])|(\.(html|json|[vo]dom|cdomc?))$/i.test(s);
554
554
 
555
- const fetchContent = async (src) => {
555
+ /**
556
+ * Resolves request information (method, body, headers) from an element's data- attributes.
557
+ * Supports data-method and data-body.
558
+ */
559
+ const getRequestInfo = (el) => {
560
+ const domEl = el.domEl || el;
561
+ const method = (domEl.getAttribute('data-method') || 'GET').toUpperCase();
562
+ const bodyAttr = domEl.getAttribute('data-body');
563
+ let body = null;
564
+ let headers = {};
565
+
566
+ if (bodyAttr) {
567
+ if (bodyAttr.startsWith('javascript:')) {
568
+ const expr = bodyAttr.slice(11);
569
+ const LV = globalThis.Lightview;
570
+ try {
571
+ // Use the registry to resolve signals/state mentioned in expressions
572
+ body = new Function('state', 'signal', `return ${expr}`)(LV.state || {}, LV.signal || {});
573
+ } catch (e) {
574
+ console.warn(`[LightviewX] Failed to evaluate data-body expression: ${expr}`, e);
575
+ }
576
+ } else if (bodyAttr.startsWith('json:')) {
577
+ try {
578
+ body = JSON.parse(bodyAttr.slice(5));
579
+ headers['Content-Type'] = 'application/json';
580
+ } catch (e) {
581
+ console.warn(`[LightviewX] Failed to parse data-body JSON: ${bodyAttr.slice(5)}`, e);
582
+ }
583
+ } else if (bodyAttr.startsWith('text:')) {
584
+ body = bodyAttr.slice(5);
585
+ headers['Content-Type'] = 'text/plain';
586
+ } else {
587
+ // Assume CSS Selector
588
+ try {
589
+ const target = document.querySelector(bodyAttr);
590
+ if (target) {
591
+ if (target.tagName === 'FORM') {
592
+ body = new FormData(target);
593
+ } else if (['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) {
594
+ const name = target.getAttribute('name') || 'body';
595
+ body = { [name]: target.value };
596
+ } else {
597
+ body = target.innerText;
598
+ }
599
+ }
600
+ } catch (e) {
601
+ // Not a valid selector, use raw string
602
+ body = bodyAttr;
603
+ }
604
+ }
605
+ }
606
+
607
+ return { method, body, headers };
608
+ };
609
+
610
+ /**
611
+ * Fetches content from a URL, respecting data-method and data-body settings.
612
+ */
613
+ const fetchContent = async (src, requestOptions = {}) => {
614
+ const { method = 'GET', body = null, headers = {} } = requestOptions;
556
615
  try {
557
616
  const LV = globalThis.Lightview;
558
617
  if (LV?.hooks?.validateUrl && !LV.hooks.validateUrl(src)) {
559
618
  console.warn(`[LightviewX] Fetch blocked by validateUrl hook: ${src}`);
560
619
  return null;
561
620
  }
562
- const url = new URL(src, document.baseURI);
563
- const res = await fetch(url);
621
+
622
+ let url = new URL(src, document.baseURI);
623
+ const fetchOptions = { method, headers: { ...headers } };
624
+
625
+ if (body) {
626
+ if (method === 'GET') {
627
+ const params = new URLSearchParams(url.search);
628
+ if (body instanceof FormData) {
629
+ for (const [key, value] of body.entries()) params.append(key, value);
630
+ } else if (typeof body === 'object' && body !== null) {
631
+ for (const [key, value] of Object.entries(body)) params.append(key, String(value));
632
+ } else {
633
+ params.append('body', String(body));
634
+ }
635
+ const queryString = params.toString();
636
+ if (queryString) {
637
+ url = new URL(`${url.origin}${url.pathname}?${queryString}${url.hash}`, url.origin);
638
+ }
639
+ } else {
640
+ if (body instanceof FormData) {
641
+ fetchOptions.body = body;
642
+ } else if (typeof body === 'object' && body !== null) {
643
+ if (headers['Content-Type'] === 'application/json' || !headers['Content-Type']) {
644
+ fetchOptions.body = JSON.stringify(body);
645
+ fetchOptions.headers['Content-Type'] = 'application/json';
646
+ } else {
647
+ fetchOptions.body = String(body);
648
+ }
649
+ } else {
650
+ fetchOptions.body = String(body);
651
+ }
652
+ }
653
+ }
654
+
655
+ const res = await fetch(url, fetchOptions);
564
656
  if (!res.ok) return null;
565
657
  const ext = url.pathname.split('.').pop().toLowerCase();
566
658
  const isJson = (ext === 'vdom' || ext === 'odom' || ext === 'cdom');
@@ -626,7 +718,11 @@ const elementsFromSelector = (selector, element) => {
626
718
  };
627
719
 
628
720
  const updateTargetContent = (el, elements, raw, loc, contentHash, options, targetHash = null) => {
629
- const { element, setupChildren, saveScrolls, restoreScrolls } = { ...options, ...globalThis.Lightview?.internals };
721
+ const { element, setupChildren, saveScrolls, restoreScrolls } = {
722
+ element: globalThis.Lightview?.element,
723
+ ...globalThis.Lightview?.internals,
724
+ ...options
725
+ };
630
726
  const markerId = `${loc}-${contentHash.slice(0, 8)}`;
631
727
  let track = getOrSet(insertedContentMap, el.domEl, () => ({}));
632
728
  if (track[loc]) removeInsertedContent(el.domEl, `${loc}-${track[loc].slice(0, 8)}`);
@@ -683,7 +779,8 @@ const handleSrcAttribute = async (el, src, tagName, { element, setupChildren })
683
779
  if (src.includes('#')) {
684
780
  [src, targetHash] = src.split('#');
685
781
  }
686
- const result = await fetchContent(src);
782
+ const options = getRequestInfo(el);
783
+ const result = await fetchContent(src, options);
687
784
  if (result) {
688
785
  elements = parseElements(result.content, result.isJson, result.isHtml, el, element, result.isCdom, result.ext);
689
786
  raw = result.raw;
@@ -747,7 +844,7 @@ const parseTargetWithLocation = (targetStr) => {
747
844
  * Intercepts clicks on elements with 'href' attributes that are not standard links.
748
845
  * Enables HTMX-like SPA navigation by loading the href content into a target element.
749
846
  */
750
- const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
847
+ const handleNonStandardHref = async (e, { domToElement, wrapDomElement }) => {
751
848
  const clickedEl = e.target.closest('[href]');
752
849
  if (!clickedEl) return;
753
850
 
@@ -764,6 +861,9 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
764
861
  }
765
862
  const targetAttr = clickedEl.getAttribute('target');
766
863
 
864
+ // Get request configuration (method, body) from the clicked element
865
+ const options = getRequestInfo(clickedEl);
866
+
767
867
  // Case 1: No target attribute - existing behavior (load into self)
768
868
  if (!targetAttr) {
769
869
  let el = domToElement.get(clickedEl);
@@ -772,6 +872,7 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
772
872
  for (let attr of clickedEl.attributes) attrs[attr.name] = attr.value;
773
873
  el = wrapDomElement(clickedEl, tagName, attrs);
774
874
  }
875
+ // Setting src triggers handleSrcAttribute via property proxy
775
876
  const newAttrs = { ...el.attributes, src: href };
776
877
  el.attributes = newAttrs;
777
878
  return;
@@ -779,21 +880,16 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
779
880
 
780
881
  // Case 2: Target starts with _ (browser navigation)
781
882
  if (targetAttr.startsWith('_')) {
883
+ if (options.method !== 'GET') {
884
+ console.warn('[LightviewX] Cannot use non-GET method for browser navigation (_blank, _top, etc.)');
885
+ }
886
+
782
887
  switch (targetAttr) {
783
- case '_self':
784
- globalThis.location.href = href;
785
- break;
786
- case '_parent':
787
- globalThis.parent.location.href = href;
788
- break;
789
- case '_top':
790
- globalThis.top.location.href = href;
791
- break;
888
+ case '_self': globalThis.location.href = href; break;
889
+ case '_parent': globalThis.parent.location.href = href; break;
890
+ case '_top': globalThis.top.location.href = href; break;
792
891
  case '_blank':
793
- default:
794
- // _blank or any custom _name opens a new window/tab
795
- globalThis.open(href, targetAttr);
796
- break;
892
+ default: globalThis.open(href, targetAttr); break;
797
893
  }
798
894
  return;
799
895
  }
@@ -803,6 +899,15 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
803
899
 
804
900
  try {
805
901
  const targetElements = document.querySelectorAll(selector);
902
+ if (targetElements.length === 0) return;
903
+
904
+ // Perform the fetch once for all targets
905
+ const result = await fetchContent(href, options);
906
+ if (!result) return;
907
+
908
+ const { setupChildren } = LV.internals;
909
+ const element = LV.element;
910
+
806
911
  targetElements.forEach(targetEl => {
807
912
  let el = domToElement.get(targetEl);
808
913
  if (!el) {
@@ -811,15 +916,17 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
811
916
  el = wrapDomElement(targetEl, targetEl.tagName.toLowerCase(), attrs);
812
917
  }
813
918
 
814
- // Build new attributes
815
- const newAttrs = { ...el.attributes, src: href };
816
- if (location) {
817
- newAttrs.location = location;
818
- }
819
- el.attributes = newAttrs;
919
+ const elements = parseElements(result.content, result.isJson, result.isHtml, el, element, result.isCdom, result.ext);
920
+ const loc = (location || targetEl.getAttribute('location') || 'innerhtml').toLowerCase();
921
+ const contentHash = hashContent(result.raw);
922
+
923
+ updateTargetContent(el, elements, result.raw, loc, contentHash, { element, setupChildren });
924
+
925
+ // Update the src attribute on the target to reflect current content
926
+ targetEl.setAttribute('src', href);
820
927
  });
821
928
  } catch (err) {
822
- console.warn('Invalid target selector:', selector, err);
929
+ console.warn('Invalid target selector or fetch error:', selector, err);
823
930
  }
824
931
  };
825
932
 
@@ -1190,6 +1297,8 @@ const setupSrcObserver = (LV) => {
1190
1297
  // Auto-register with Lightview if available
1191
1298
  if (typeof window !== 'undefined' && globalThis.Lightview) {
1192
1299
  const LV = globalThis.Lightview;
1300
+ LV.state = state;
1301
+ LV.getState = getState;
1193
1302
 
1194
1303
  // Extend Lightview with simple named signal getter/setter if needed (already in Core now)
1195
1304
  // But for template literals we use processTemplateChild which needs access to registries
@@ -1696,8 +1805,23 @@ if (lvInternals) {
1696
1805
  }
1697
1806
 
1698
1807
  // Export for module usage
1808
+ const request = async (el) => {
1809
+ const domEl = el.domEl || el;
1810
+ const href = domEl.getAttribute('href');
1811
+ if (!href) return;
1812
+ return handleNonStandardHref({
1813
+ target: domEl,
1814
+ preventDefault: () => { },
1815
+ stopPropagation: () => { }
1816
+ }, {
1817
+ domToElement: globalThis.Lightview.internals.domToElement,
1818
+ wrapDomElement: globalThis.Lightview.internals.wrapDomElement
1819
+ });
1820
+ };
1821
+
1699
1822
  const LightviewX = {
1700
1823
  state,
1824
+ getState,
1701
1825
  themeSignal,
1702
1826
  setTheme,
1703
1827
  registerStyleSheet,
@@ -1705,6 +1829,12 @@ const LightviewX = {
1705
1829
  // Gate modifiers
1706
1830
  throttle: gateThrottle,
1707
1831
  debounce: gateDebounce,
1832
+ // Hypermedia Actions
1833
+ request,
1834
+ get: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'GET'); return request(el); },
1835
+ post: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'POST'); return request(el); },
1836
+ put: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'PUT'); return request(el); },
1837
+ delete: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'DELETE'); return request(el); },
1708
1838
  // Component initialization
1709
1839
  initComponents,
1710
1840
  componentConfig,
@@ -1719,6 +1849,7 @@ const LightviewX = {
1719
1849
  parseElements
1720
1850
  }
1721
1851
  };
1852
+ if (globalThis.Lightview) globalThis.Lightview.request = request;
1722
1853
 
1723
1854
  if (typeof module !== 'undefined' && module.exports) {
1724
1855
  module.exports = LightviewX;
package/src/lightview.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { signal, effect, computed, getRegistry, internals } from './reactivity/signal.js';
2
- import { state, getState } from './reactivity/state.js';
3
2
 
4
3
  const core = {
5
4
  get currentEffect() {
@@ -164,6 +163,7 @@ const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
164
163
  tag,
165
164
  attributes,
166
165
  children,
166
+ isProxy: true,
167
167
  get domEl() { return domNode; }
168
168
  };
169
169
  const proxy = makeReactive(el);
@@ -171,6 +171,15 @@ const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
171
171
  return proxy;
172
172
  };
173
173
 
174
+ /**
175
+ * Recursively checks if any item in a nested array matches a predicate.
176
+ * Replaces expensive .flat(Infinity).some() calls.
177
+ */
178
+ const someRecursive = (item, predicate) => {
179
+ if (Array.isArray(item)) return item.some(i => someRecursive(i, predicate));
180
+ return predicate(item);
181
+ };
182
+
174
183
  /**
175
184
  * The core virtual-DOM-to-real-DOM factory.
176
185
  * Handles tag functions (components), shadow DOM directives, and SVG namespaces.
@@ -199,12 +208,17 @@ const element = (tag, attributes = {}, children = []) => {
199
208
  };
200
209
 
201
210
  const update = () => {
202
- const flat = (Array.isArray(el.children) ? el.children : [el.children]).flat(Infinity);
203
- const bits = flat.map(c => {
211
+ const bits = [];
212
+ const walk = (c) => {
213
+ if (Array.isArray(c)) {
214
+ for (let i = 0; i < c.length; i++) walk(c[i]);
215
+ return;
216
+ }
204
217
  const val = typeof c === 'function' ? c() : c;
205
- if (val && typeof val === 'object' && val.domEl) return val.domEl.textContent;
206
- return (val === null || val === undefined) ? '' : String(val);
207
- });
218
+ if (val && typeof val === 'object' && val.domEl) bits.push(val.domEl.textContent);
219
+ else bits.push((val === null || val === undefined) ? '' : String(val));
220
+ };
221
+ walk(el.children);
208
222
  domNode.textContent = bits.join(' ');
209
223
  };
210
224
 
@@ -216,7 +230,7 @@ const element = (tag, attributes = {}, children = []) => {
216
230
  }
217
231
  });
218
232
 
219
- const hasReactive = children.flat(Infinity).some(c => typeof c === 'function');
233
+ const hasReactive = someRecursive(children, c => typeof c === 'function');
220
234
  if (hasReactive) {
221
235
  const runner = effect(update);
222
236
  trackEffect(domNode, runner);
@@ -233,12 +247,29 @@ const element = (tag, attributes = {}, children = []) => {
233
247
  ? document.createElementNS('http://www.w3.org/2000/svg', tag)
234
248
  : document.createElement(tag);
235
249
 
236
- const proxy = wrapDomElement(domNode, tag, attributes, children);
237
- proxy.attributes = attributes;
238
- proxy.children = children;
250
+ // Optimization: Skip Proxy allocation for static nodes
251
+ const hasReactiveAttr = Object.values(attributes).some(v => typeof v === 'function');
252
+ const hasReactiveChild = someRecursive(children, c => typeof c === 'function' || (c && c.isProxy));
253
+
254
+ if (hasReactiveAttr || hasReactiveChild) {
255
+ const proxy = wrapDomElement(domNode, tag, attributes, children);
256
+ proxy.attributes = attributes;
257
+ proxy.children = children;
258
+ if (isSVG) inSVG = wasInSVG;
259
+ return proxy;
260
+ }
261
+
262
+ // Static path: Direct application to the DOM node
263
+ makeReactiveAttributes(attributes, domNode);
264
+ setupChildren(children, domNode);
239
265
 
240
266
  if (isSVG) inSVG = wasInSVG;
241
- return proxy;
267
+ return {
268
+ tag,
269
+ attributes,
270
+ children,
271
+ domEl: domNode
272
+ };
242
273
  };
243
274
 
244
275
  // Process component function return value (HTML string, DOM node, vDOM, or Object DOM)
@@ -254,7 +285,7 @@ const processComponentResult = (result) => {
254
285
 
255
286
  const type = typeof result;
256
287
  // DOM node - wrap it
257
- if (type === 'object' && result instanceof HTMLElement) {
288
+ if (type === 'object' && result && result.nodeType === 1) {
258
289
  return wrapDomElement(result, result.tagName.toLowerCase(), {}, []);
259
290
  }
260
291
 
@@ -271,7 +302,7 @@ const processComponentResult = (result) => {
271
302
  template.innerHTML = result.trim();
272
303
  const content = template.content;
273
304
  // If single element, return it; otherwise wrap in a fragment-like span
274
- if (content.childNodes.length === 1 && content.firstChild instanceof HTMLElement) {
305
+ if (content.childNodes.length === 1 && content.firstChild && content.firstChild.nodeType === 1) {
275
306
  const el = content.firstChild;
276
307
  return wrapDomElement(el, el.tagName.toLowerCase(), {}, []);
277
308
  } else {
@@ -335,12 +366,14 @@ const setAttributeValue = (domNode, key, value) => {
335
366
  /**
336
367
  * Processes attributes, handling event listeners, reactive bindings, and special 'onmount' hooks.
337
368
  */
338
- const makeReactiveAttributes = (attributes, domNode) => {
369
+ const makeReactiveAttributes = (attributes = {}, domNode) => {
339
370
  const reactiveAttrs = {};
340
371
 
341
- for (let [key, value] of Object.entries(attributes)) {
372
+ for (const key in attributes) {
373
+ const value = attributes[key];
374
+ const type = typeof value;
342
375
  // Handle XPath markers from hydration
343
- if (value && typeof value === 'object' && value.__xpath__ && value.__static__) {
376
+ if (value && type === 'object' && value.__xpath__ && value.__static__) {
344
377
  // Mark attribute for later XPath resolution
345
378
  domNode.setAttribute(`data-xpath-${key}`, value.__xpath__);
346
379
  reactiveAttrs[key] = value;
@@ -356,11 +389,10 @@ const makeReactiveAttributes = (attributes, domNode) => {
356
389
  }
357
390
  } else if (key.startsWith('on')) {
358
391
  // Event handler
359
- if (typeof value === 'function') {
392
+ if (type === 'function') {
360
393
  // Function handler - use addEventListener
361
- const eventName = key.slice(2).toLowerCase();
362
- domNode.addEventListener(eventName, value);
363
- } else if (typeof value === 'string') {
394
+ domNode.addEventListener(key.slice(2).toLowerCase(), value);
395
+ } else if (type === 'string') {
364
396
  // String handler (from parsed HTML) - use setAttribute
365
397
  // Browser will compile the string into a handler function
366
398
  domNode.setAttribute(key, value);
@@ -372,20 +404,21 @@ const makeReactiveAttributes = (attributes, domNode) => {
372
404
  reactiveAttrs[key] = processed;
373
405
  } else if (key === 'style') {
374
406
  // Style object support (merged from below)
375
- Object.entries(value).forEach(([styleKey, styleValue]) => {
407
+ for (const styleKey in entries) {
408
+ const styleValue = entries[styleKey];
376
409
  if (typeof styleValue === 'function') {
377
410
  const runner = effect(() => { domNode.style[styleKey] = styleValue(); });
378
411
  trackEffect(domNode, runner);
379
412
  } else {
380
413
  domNode.style[styleKey] = styleValue;
381
414
  }
382
- });
415
+ }
383
416
  reactiveAttrs[key] = value;
384
417
  } else {
385
418
  setAttributeValue(domNode, key, value);
386
419
  reactiveAttrs[key] = value;
387
420
  }
388
- } else if (typeof value === 'function') {
421
+ } else if (type === 'function') {
389
422
  // Reactive binding
390
423
  const runner = effect(() => {
391
424
  const result = value();
@@ -429,29 +462,33 @@ const processChildren = (children, targetNode, clearExisting = true) => {
429
462
  const isSpecialElement = targetNode.tagName &&
430
463
  (targetNode.tagName.toLowerCase() === 'script' || targetNode.tagName.toLowerCase() === 'style');
431
464
 
432
- const flatChildren = children.flat(Infinity);
465
+ const walk = (child) => {
466
+ if (Array.isArray(child)) {
467
+ for (let i = 0; i < child.length; i++) walk(child[i]);
468
+ return;
469
+ }
470
+
471
+ if (child === null || child === undefined) return;
433
472
 
434
- for (let child of flatChildren) {
435
473
  // Allow extensions to transform children (e.g., template literals)
436
474
  // BUT skip for script/style elements which need raw content
437
475
  if (Lightview.hooks.processChild && !isSpecialElement) {
438
476
  child = Lightview.hooks.processChild(child) ?? child;
439
477
  }
440
478
 
441
- // Handle shadowDOM markers - attach shadow to parent and process shadow children
442
- if (isShadowDOMMarker(child)) {
443
- // targetNode is the parent element that should get the shadow root
444
- // For ShadowRoot targets, we can't attach another shadow, so warn
445
- if (targetNode instanceof ShadowRoot) {
446
- console.warn('Lightview: Cannot nest shadowDOM inside another shadowDOM');
447
- continue;
448
- }
449
- processShadowDOM(child, targetNode);
450
- continue;
451
- }
452
-
453
479
  const type = typeof child;
454
- if (type === 'function') {
480
+
481
+ if (child && type === 'object' && child.tag) {
482
+ // 1. Child element (already wrapped or plain object) - tag can be string or function (structural)
483
+ const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
484
+ targetNode.appendChild(childEl.domEl);
485
+ childElements.push(childEl);
486
+ } else if (['string', 'number', 'boolean', 'symbol'].includes(type) || (child && type === 'object' && child instanceof String)) {
487
+ // 2. Static text (common leaf)
488
+ targetNode.appendChild(document.createTextNode(child));
489
+ childElements.push(child);
490
+ } else if (type === 'function') {
491
+ // 3. Reactive function
455
492
  const startMarker = document.createComment('lv:s');
456
493
  const endMarker = document.createComment('lv:e');
457
494
  targetNode.appendChild(startMarker);
@@ -462,7 +499,6 @@ const processChildren = (children, targetNode, clearExisting = true) => {
462
499
  // 1. Cleanup: Remove everything between markers
463
500
  while (startMarker.nextSibling && startMarker.nextSibling !== endMarker) {
464
501
  startMarker.nextSibling.remove();
465
- // Note: MutationObserver handles cleanupNode(removedNode)
466
502
  }
467
503
 
468
504
  // 2. Execution: Get new value and process it
@@ -491,20 +527,10 @@ const processChildren = (children, targetNode, clearExisting = true) => {
491
527
  runner = effect(update);
492
528
  trackEffect(startMarker, runner);
493
529
  childElements.push(child);
494
- } else if (child && typeof child === 'object' && child.__xpath__ && child.__static__) {
495
- // XPath marker - create text node with marker for later resolution
496
- const textNode = document.createTextNode('');
497
- textNode.__xpathExpr = child.__xpath__;
498
- targetNode.appendChild(textNode);
499
- childElements.push(child);
500
- } else if (['string', 'number', 'boolean', 'symbol'].includes(type) || (child && type === 'object' && child instanceof String)) {
501
- // Static text
502
- targetNode.appendChild(document.createTextNode(child));
503
- childElements.push(child);
504
530
  } else if (child instanceof Node) {
505
- // Raw DOM node
531
+ // 4. Raw DOM node
506
532
  const node = child.domEl || child;
507
- if (node instanceof HTMLElement || node instanceof SVGElement) {
533
+ if (node.nodeType === 1) { // ELEMENT_NODE
508
534
  const wrapped = wrapDomElement(node, node.tagName.toLowerCase());
509
535
  targetNode.appendChild(node);
510
536
  childElements.push(wrapped);
@@ -512,13 +538,23 @@ const processChildren = (children, targetNode, clearExisting = true) => {
512
538
  targetNode.appendChild(node);
513
539
  childElements.push(child);
514
540
  }
515
- } else if (child && type === 'object' && child.tag) {
516
- // Child element (already wrapped or plain object) - tag can be string or function
517
- const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
518
- targetNode.appendChild(childEl.domEl);
519
- childElements.push(childEl);
541
+ } else if (isShadowDOMMarker(child)) {
542
+ // 5. Shadow DOM marker
543
+ if (targetNode instanceof ShadowRoot) {
544
+ console.warn('Lightview: Cannot nest shadowDOM inside another shadowDOM');
545
+ return;
546
+ }
547
+ processShadowDOM(child, targetNode);
548
+ } else if (child && typeof child === 'object' && child.__xpath__ && child.__static__) {
549
+ // 6. XPath marker
550
+ const textNode = document.createTextNode('');
551
+ textNode.__xpathExpr = child.__xpath__;
552
+ targetNode.appendChild(textNode);
553
+ childElements.push(child);
520
554
  }
521
- }
555
+ };
556
+
557
+ walk(children);
522
558
 
523
559
 
524
560
 
@@ -551,7 +587,7 @@ const enhance = (selectorOrNode, options = {}) => {
551
587
 
552
588
  // If it's already a Lightview element, use its domEl
553
589
  const node = domNode.domEl || domNode;
554
- if (!(node instanceof HTMLElement)) return null;
590
+ if (!node || node.nodeType !== 1) return null;
555
591
 
556
592
  const tagName = node.tagName.toLowerCase();
557
593
  let el = domToElement.get(node);
@@ -670,8 +706,6 @@ const tags = new Proxy({}, {
670
706
  });
671
707
 
672
708
  const Lightview = {
673
- state,
674
- getState,
675
709
  registerSchema: (name, definition) => internals.schemas.set(name, definition),
676
710
  signal,
677
711
  get: signal.get,