lightview 2.3.8 → 2.4.7

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 (45) hide show
  1. package/.gemini/CODE_ANALYSIS_AND_IMPROVEMENT_PLAN.md +56 -0
  2. package/AI-GUIDANCE.md +274 -0
  3. package/README.md +35 -0
  4. package/build_tmp/lightview-cdom.js +3934 -0
  5. package/build_tmp/lightview-router.js +185 -0
  6. package/build_tmp/lightview-x.js +1739 -0
  7. package/build_tmp/lightview.js +740 -0
  8. package/components/data-display/diff.js +36 -4
  9. package/docs/api/hypermedia.html +75 -5
  10. package/docs/api/index.html +3 -3
  11. package/docs/api/nav.html +0 -16
  12. package/docs/articles/html-vs-json-partials.md +102 -0
  13. package/docs/articles/lightview-vs-htmx.md +610 -0
  14. package/docs/assets/styles/site.css +16 -7
  15. package/docs/benchmarks/bau-tagged-fragment.js +41 -0
  16. package/docs/benchmarks/tagged-fragment.js +36 -0
  17. package/docs/cdom.html +127 -88
  18. package/docs/components/chart.html +157 -210
  19. package/docs/components/component-nav.html +1 -1
  20. package/docs/components/diff.html +33 -21
  21. package/docs/components/gallery.html +107 -4
  22. package/docs/components/index.css +18 -3
  23. package/docs/components/index.html +20 -9
  24. package/docs/dom-benchmark.html +771 -0
  25. package/docs/getting-started/index.html +42 -2
  26. package/docs/hypermedia/index.html +391 -0
  27. package/docs/hypermedia/nav.html +17 -0
  28. package/docs/index.html +136 -17
  29. package/index.html +59 -10
  30. package/lightview-all.js +223 -67
  31. package/lightview-cdom.js +1 -2
  32. package/lightview-x.js +144 -13
  33. package/lightview.js +85 -277
  34. package/package.json +2 -2
  35. package/src/lightview-cdom.js +1 -5
  36. package/src/lightview-x.js +158 -27
  37. package/src/lightview.js +94 -60
  38. package/docs/articles/calculator-no-javascript-hackernoon.md +0 -283
  39. package/docs/articles/calculator-no-javascript.md +0 -290
  40. package/docs/articles/part1-reference.md +0 -236
  41. package/lightview.js.bak +0 -1
  42. package/test-xpath.html +0 -63
  43. package/test_error.txt +0 -0
  44. package/test_output.txt +0 -0
  45. package/test_output_full.txt +0 -0
@@ -0,0 +1,41 @@
1
+ (async function () {
2
+ // In ES modules, document.currentScript is null, so we use import.meta.url
3
+ const urlParams = new URLSearchParams(import.meta.url.split('?')[1]);
4
+ const count = parseInt(urlParams.get('count')) || 1000;
5
+
6
+ // Settle inside the script context
7
+ if (window.gc) window.gc();
8
+ await new Promise(r => setTimeout(r, 100));
9
+
10
+ const start = performance.now();
11
+
12
+ // Import Bau from CDN
13
+ const { default: Bau } = await import('https://cdn.jsdelivr.net/npm/@grucloud/bau@0.106.0/+esm');
14
+ const bau = Bau();
15
+ const { section, header, h2, h3, div, article, p, span, footer } = bau.tags;
16
+
17
+ const items = [];
18
+ for (let i = 0; i < count; i++) {
19
+ items.push(article({ class: 'item-card' },
20
+ header(h3({ class: 'item-title' }, `Item ${i}`)),
21
+ div({ class: 'item-content' }, p({ class: 'item-description' }, `Detailed description for item ${i} in the benchmark list.`)),
22
+ footer({ class: 'item-footer' }, span(`Metadata for item ${i}`))
23
+ ));
24
+ }
25
+ const fragment = section({ class: 'benchmark-container' },
26
+ header(h2('Benchmark Results')),
27
+ div({ class: 'items-grid' }, ...items)
28
+ );
29
+ const target = document.getElementById('benchmark-target');
30
+ target.replaceChildren(fragment);
31
+
32
+ const end = performance.now();
33
+
34
+ // Store timing in a global for the driver to pick up
35
+ window.__bauTaggedBenchmarkTime = end - start;
36
+
37
+ // Signal completion
38
+ if (window.__resolveBauTaggedBenchmark) {
39
+ window.__resolveBauTaggedBenchmark();
40
+ }
41
+ })();
@@ -0,0 +1,36 @@
1
+ (async function () {
2
+ const urlParams = new URLSearchParams(document.currentScript.src.split('?')[1]);
3
+ const count = parseInt(urlParams.get('count')) || 1000;
4
+
5
+ // Settle inside the script context
6
+ if (window.gc) window.gc();
7
+ await new Promise(r => setTimeout(r, 100));
8
+
9
+ const start = performance.now();
10
+
11
+ const { section, header, h2, h3, div, article, p, span, footer } = Lightview.tags;
12
+ const items = [];
13
+ for (let i = 0; i < count; i++) {
14
+ items.push(article({ class: 'item-card' },
15
+ header(h3({ class: 'item-title' }, `Item ${i}`)),
16
+ div({ class: 'item-content' }, p({ class: 'item-description' }, `Detailed description for item ${i} in the benchmark list.`)),
17
+ footer({ class: 'item-footer' }, span(`Metadata for item ${i}`))
18
+ ));
19
+ }
20
+ const fragment = section({ class: 'benchmark-container' },
21
+ header(h2('Benchmark Results')),
22
+ div({ class: 'items-grid' }, ...items)
23
+ );
24
+ const target = document.getElementById('benchmark-target');
25
+ target.replaceChildren(fragment.domEl);
26
+
27
+ const end = performance.now();
28
+
29
+ // Store timing in a global for the driver to pick up
30
+ window.__taggedBenchmarkTime = end - start;
31
+
32
+ // Signal completion
33
+ if (window.__resolveTaggedBenchmark) {
34
+ window.__resolveTaggedBenchmark();
35
+ }
36
+ })();
package/docs/cdom.html CHANGED
@@ -53,7 +53,7 @@
53
53
 
54
54
  <script>
55
55
  globalThis.switchCDOMTab = (tabId) => {
56
- const tabs = ['jprx', 'jprxc', 'operators', 'embedded'];
56
+ const tabs = ['jprx', 'cdomc', 'operators'];
57
57
  tabs.forEach(t => {
58
58
  const btn = document.getElementById(`tab-btn-${t}`);
59
59
  const pane = document.getElementById(`pane-${t}`);
@@ -132,13 +132,14 @@ const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
132
132
  const { $ } = Lightview;
133
133
 
134
134
  const cdom = `{
135
- div: {
136
- onmount: =state({ count: 0 }, { name: 'local', schema: 'auto', scope: $this }),
137
- children: [
138
- { h2: "Counter" },
139
- { p: ["Count: ", =/local/count] },
140
- { button: { onclick: =++/local/count, children: ["+"] } },
141
- { button: { onclick: =--/local/count, children: ["-"] } }
135
+ "div": {
136
+ "id": "Counter",
137
+ "onmount": "=state({ count: 0 }, { name: 'local', schema: 'auto', scope: $this })",
138
+ "children": [
139
+ { "h2": "#../../@id" }, // XPath shorthand: text node -> h2 -> div
140
+ { "p": ["Count: ", "=/local/count"] },
141
+ { "button": { "onclick": "=++/local/count", "children": ["+"] } },
142
+ { "button": { "onclick": "=--/local/count", "children": ["-"] } }
142
143
  ]
143
144
  }
144
145
  }`;
@@ -150,14 +151,44 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
150
151
  </p>
151
152
  <ul>
152
153
  <li><code>onmount</code> — A Lightview lifecycle hook where we initialize state.</li>
154
+ <li><code>#../../@id</code> — An XPath expression that extracts the <code>id</code> from the grandparent
155
+ element.</li>
153
156
  <li><code>=state(...)</code> — An initializer that creates local reactive state.</li>
154
157
  <li><code>scope: $this</code> — Explicitly attaches the state to the current element.</li>
155
158
  <li><code>=/local/count</code> — A path that reactively displays the count value.</li>
156
159
  </ul>
160
+ <p style="font-size: 0.95rem; font-style: italic; color: var(--site-text-secondary);">
161
+ <strong>Note:</strong> In cDOM, <code>#</code> is the prefix for <strong>XPath</strong> expressions that
162
+ navigate and extract data from the DOM, while <code>=</code> is the prefix for <strong>JPRX</strong>
163
+ expressions that navigate or apply functions to
164
+ reactive state.
165
+ </p>
157
166
  <p>
158
- The UI automatically updates whenever <code>count</code> changes — no manual DOM manipulation required.
167
+ The UI automatically updates whenever <code>count</code> changes — no manual DOM manipulation required. It
168
+ also uses XPath to navigate the DOM structure during construction.
159
169
  </p>
160
170
 
171
+ <!-- ===== ADVANTAGES ===== -->
172
+ <h2 id="advantages">Advantages</h2>
173
+ <div class="feature-grid" style="margin: 2rem 0;">
174
+ <div class="feature-card">
175
+ <h3 class="feature-title">🛡️ Enhanced Security</h3>
176
+ <p>
177
+ cDOM strictly avoids <code>eval()</code> and direct HTML injection. By using a custom
178
+ high-performance parser and a registry of pre-defined helper functions, it provides a safe sandbox
179
+ for dynamic content, making it highly resistant to XSS attacks.
180
+ </p>
181
+ </div>
182
+ <div class="feature-card">
183
+ <h3 class="feature-title">🤖 LLM Friendly</h3>
184
+ <p>
185
+ Large Language Models excel at generating structured data and formulaic expressions. cDOM's
186
+ declarative nature and concise syntax make it far easier for AI to generate correct,
187
+ bug-free UI components compared to traditional JavaScript-heavy frameworks.
188
+ </p>
189
+ </div>
190
+ </div>
191
+
161
192
  <!-- ===== XPATH NAVIGATION ===== -->
162
193
  <h2 id="xpath-navigation">Using XPath</h2>
163
194
  <p>
@@ -184,10 +215,13 @@ const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
184
215
  const { $ } = Lightview;
185
216
 
186
217
  const cdom = `{
218
+ // The cdom below uses a compressed syntax. It does not require quotes
219
+ // around properties or JPRX/XPath values and supports comments.
220
+ // It is JSON-like, but not JSON.
187
221
  div: {
188
222
  id: "profile-container",
189
223
  class: "card",
190
- "data-theme": "dark",
224
+ data-theme: "dark",
191
225
  children: [
192
226
  { h3: "User Profile" },
193
227
  { button: {
@@ -210,27 +244,6 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
210
244
  For more details, see the <a href="/docs/cdom-xpath.html">Full XPath Documentation</a>.
211
245
  </p>
212
246
 
213
- <!-- ===== ADVANTAGES ===== -->
214
- <h2 id="advantages">Advantages</h2>
215
- <div class="feature-grid" style="margin: 2rem 0;">
216
- <div class="feature-card">
217
- <h3 class="feature-title">🛡️ Enhanced Security</h3>
218
- <p>
219
- cDOM strictly avoids <code>eval()</code> and direct HTML injection. By using a custom
220
- high-performance parser and a registry of pre-defined helper functions, it provides a safe sandbox
221
- for dynamic content, making it highly resistant to XSS attacks.
222
- </p>
223
- </div>
224
- <div class="feature-card">
225
- <h3 class="feature-title">🤖 LLM Friendly</h3>
226
- <p>
227
- Large Language Models excel at generating structured data and formulaic expressions. cDOM's
228
- declarative nature and concise syntax make it far easier for AI to generate correct,
229
- bug-free UI components compared to traditional JavaScript-heavy frameworks.
230
- </p>
231
- </div>
232
- </div>
233
-
234
247
  <!-- ===== JPRX INTRODUCTION ===== -->
235
248
  <h2 id="JPRX">JPRX (JSON Pointer Reactive eXpressions)</h2>
236
249
  <p>
@@ -369,6 +382,44 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
369
382
  </tbody>
370
383
  </table>
371
384
 
385
+ <h3 id="JPRX-explosion">Explosion & Mapping (<code>...</code>)</h3>
386
+ <p>
387
+ The <code>...</code> operator is a powerful JPRX extension used for mapping properties from arrays and
388
+ spreading array elements as function arguments.
389
+ </p>
390
+
391
+ <h4>Mapping & Auto-Explosion: <code>path...property</code></h4>
392
+ <p>
393
+ When used between a path to an array and a property name, it acts as a shorthand for mapping. It extracts
394
+ the specified property from every object in the array. <strong>Inside a function call, it automatically
395
+ spreads (explodes) the resulting values as individual arguments.</strong>
396
+ </p>
397
+ <div class="code-block" style="margin-bottom: 1.5rem;">
398
+ <pre><code>// Correct: Maps 'price' and automatically spreads results as arguments to sum()
399
+ =sum(/cart/items...price)
400
+
401
+ // Mapping also works for direct display (returns an array)
402
+ { p: ["Prices: ", =/cart/items...price] }</code></pre>
403
+ </div>
404
+
405
+ <h4>Argument Spreading: <code>path...</code></h4>
406
+ <p>
407
+ When an array path is followed by a trailing <code>...</code> at the end of the path expression (and no
408
+ property name follows), the array elements are spread as individual arguments. Use this when you have a
409
+ direct reference to an array.
410
+ </p>
411
+ <div class="code-block">
412
+ <pre><code>// Passes each number in the array as a separate argument
413
+ =sum(/listOfNumbers...)</code></pre>
414
+ </div>
415
+
416
+ <div class="warning-notice"
417
+ style="margin-top: 1.5rem; padding: 1rem; background: rgba(239, 68, 68, 0.1); border-left: 4px solid #ef4444; border-radius: 4px;">
418
+ <strong>Warning:</strong> Do not combine infix mapping with trailing dots. <code>/items...price...</code> is
419
+ invalid because the trailing dots are interpreted as part of the property name (looking for a property
420
+ literally named "price...").
421
+ </div>
422
+
372
423
  <!-- ===== COMPARISON TO EXCEL ===== -->
373
424
  <h2 id="comparison">Comparison to Excel</h2>
374
425
  <p>
@@ -536,8 +587,37 @@ $('#example').content(hydrate(parseJPRX(cdom)));</code></pre>
536
587
  <h3 id="lifecycle-state">Lifecycle State</h3>
537
588
  <p>
538
589
  In Lightview, you initialize state within the <code>onmount</code> hook. The <code>=state</code> and
539
- <code>=signal</code> helpers accept an options object where you can specify a <code>scope</code>,
540
- as well as <code>schema</code> requirements.
590
+ <code>=signal</code> helpers allow you to create reactive data that is tied to the DOM structure.
591
+ </p>
592
+
593
+ <h4 id="scope-resolution">Scope & Name Resolution</h4>
594
+ <p>
595
+ When JPRX encounters a name (e.g., <code>=/profile/name</code>), it doesn't just look in a global registry.
596
+ Instead, it performs an <strong>up-tree search</strong>:
597
+ </p>
598
+ <ol>
599
+ <li>It starts at the element where the expression is defined.</li>
600
+ <li>It looks for a registered signal or state with that name.</li>
601
+ <li>If not found, it moves to the parent element and repeats the search.</li>
602
+ <li>This continues all the way to the <code>document</code> root, finally checking the global registry if
603
+ needed.</li>
604
+ </ol>
605
+
606
+ <h4>The <code>$this</code> Placeholder</h4>
607
+ <p>
608
+ The <code>$this</code> keyword is a special placeholder that represents the <strong>current
609
+ element</strong>. When used in the <code>scope</code> option of <code>=state</code> or
610
+ <code>=signal</code>, it instructs Lightview to register the name specifically at that element's level in
611
+ the DOM.
612
+ </p>
613
+ <div class="code-block" style="margin-bottom: 1.5rem;">
614
+ <pre><code>// Scoping state to the current element creates a local namespace
615
+ { "onmount": "=state({ count: 0 }, { name: 'local', scope: $this })" }</code></pre>
616
+ </div>
617
+ <p>
618
+ This mechanism is what allows you to create <strong>reusable components</strong>. Each instance of a
619
+ component can have its own "local" state because the search for <code>local</code> will stop at the first
620
+ element it finds that has it registered—typically the root of the component instance.
541
621
  </p>
542
622
 
543
623
  <h4>Using a Registered Schema</h4>
@@ -578,10 +658,7 @@ Lightview.registerSchema('User', { name: 'string', age: 'number' });
578
658
  }
579
659
  }</code></pre>
580
660
  </div>
581
- <p>
582
- By scoping the state to the element, you can create multiple independent instances of components.
583
- Lightview uses a high-performance <strong>up-tree search</strong> to resolve these names.
584
- </p>
661
+
585
662
 
586
663
  <h3 id="bind-helper">Two-Way Binding ($bind)</h3>
587
664
  <p>
@@ -664,17 +741,15 @@ $('#example').content(hydrated);
664
741
  <!-- ===== INTERACTIVE EXAMPLE ===== -->
665
742
  <h2 id="interactive-example">Interactive Example</h2>
666
743
  <p>
667
- Choose a syntax to see how the same reactive counter can be defined using standard JSON, concise JPRXC, or
744
+ Choose a syntax to see how the same reactive counter can be defined using standard JSON, concise cDOMC, or
668
745
  operator syntax.
669
746
  </p>
670
747
 
671
748
  <div role="tablist" class="syntax-tabs">
672
749
  <button id="tab-btn-jprx" class="syntax-tab syntax-tab-active" onclick="switchCDOMTab('jprx')">JPRX
673
750
  (Standard)</button>
674
- <button id="tab-btn-jprxc" class="syntax-tab" onclick="switchCDOMTab('jprxc')">JPRXC (Concise)</button>
751
+ <button id="tab-btn-cdomc" class="syntax-tab" onclick="switchCDOMTab('cdomc')">cDOMC</button>
675
752
  <button id="tab-btn-operators" class="syntax-tab" onclick="switchCDOMTab('operators')">Operators</button>
676
- <button id="tab-btn-embedded" class="syntax-tab" onclick="switchCDOMTab('embedded')">Embedded
677
- Signals</button>
678
753
  </div>
679
754
 
680
755
  <!-- JPRX Pane -->
@@ -690,12 +765,11 @@ $('#example').content(hydrated);
690
765
  </script><code contenteditable="true">// JPRX: Standard JSON format (strict)
691
766
  await import('/lightview-cdom.js');
692
767
  const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
693
- const { signal, $ } = Lightview;
694
-
695
- const count = signal(0, 'count');
768
+ const { $ } = Lightview;
696
769
 
697
770
  const cdomString = `{
698
771
  "div": {
772
+ onmount: "=signal(0, 'count')",
699
773
  "children": [
700
774
  { "h3": ["Standard JPRX Counter"] },
701
775
  { "p": { "children": ["Count: ", "=/count"] }},
@@ -709,12 +783,11 @@ const cdomString = `{
709
783
 
710
784
  const hydrated = hydrate(parseJPRX(cdomString));
711
785
  $('#example').content(hydrated);
712
- globalThis.LightviewCDOM.activate(hydrated.domEl);
713
786
  </code></pre>
714
787
  </div>
715
788
 
716
- <!-- JPRXC Pane -->
717
- <div id="pane-jprxc" style="display: none;">
789
+ <!-- cDOMC Pane -->
790
+ <div id="pane-cdomc" style="display: none;">
718
791
  <pre><script>
719
792
  examplify(document.currentScript.nextElementSibling, {
720
793
  at: document.currentScript.parentElement,
@@ -722,17 +795,17 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
722
795
  type: 'module',
723
796
  height: '250px'
724
797
  });
725
- </script><code contenteditable="true">// JPRXC: Concise format (unquoted keys, comments)
798
+ </script><code contenteditable="true">// cDOMC: Compressed format (unquoted keys, comments)
726
799
  await import('/lightview-cdom.js');
727
800
  const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
728
- const { signal, $ } = Lightview;
801
+ const { $ } = ightview;
729
802
 
730
- const count = signal(0, 'count');
731
803
 
732
804
  const cdomString = `{
733
805
  div: {
806
+ onmount: =signal(0, 'count'),
734
807
  children: [
735
- { h3: ["Concise Counter"] },
808
+ { h3: ["Compressed Counter"] },
736
809
  { p: { children: ["Count: ", =/count] }},
737
810
  { div: { children: [
738
811
  { button: { onclick: =decrement(/count), children: ["-"] } },
@@ -760,12 +833,11 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
760
833
  </script><code contenteditable="true">// Operators: Using =++/path and =--/path
761
834
  await import('/lightview-cdom.js');
762
835
  const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
763
- const { signal, $ } = Lightview;
764
-
765
- const count = signal(0, 'count');
836
+ const { $ } = Lightview;
766
837
 
767
838
  const cdomString = `{
768
839
  div: {
840
+ onmount: =signal(0, 'count'),
769
841
  children: [
770
842
  { h3: ["Operator Counter"] },
771
843
  { p: { children: ["Count: ", =/count] }},
@@ -784,39 +856,6 @@ globalThis.LightviewCDOM.activate(hydrated.domEl);
784
856
  </code></pre>
785
857
  </div>
786
858
 
787
- <!-- Embedded Pane -->
788
- <div id="pane-embedded" style="display: none;">
789
- <pre><script>
790
- examplify(document.currentScript.nextElementSibling, {
791
- at: document.currentScript.parentElement,
792
- scripts: ['/lightview.js', '/lightview-x.js', '/lightview-cdom.js'],
793
- type: 'module',
794
- height: '250px'
795
- });
796
- </script><code contenteditable="true">// Embedded Signals: Using onmount to initialize local state
797
- await import('/lightview-cdom.js');
798
- const { parseJPRX, hydrate } = globalThis.LightviewCDOM;
799
- const { $ } = Lightview;
800
-
801
- const cdomString = `{
802
- div: {
803
- // Initialize a named signal explicitly scoped to '$this' element.
804
- onmount: =signal(0, { name: 'count', scope: $this }),
805
- children: [
806
- { h3: ["Embedded Counter"] },
807
- { p: ["Count: ", =/count] },
808
- { div: { children: [
809
- { button: { onclick: =--/count, children: ["-"] } },
810
- { button: { onclick: =++/count, children: ["+"] } }
811
- ]}}
812
- ]
813
- }
814
- }`;
815
-
816
- const hydrated = hydrate(parseJPRX(cdomString));
817
- $('#example').content(hydrated);
818
- </code></pre>
819
- </div>
820
859
 
821
860
  <!-- ===== OPERATOR SYNTAX EXAMPLE ===== -->
822
861
  <h2 id="operator-syntax">Operator Syntax</h2>
@@ -938,7 +977,7 @@ LightviewCDOM.activate(document.getElementById('myApp'));</code></pre>
938
977
 
939
978
  <h3 id="api-hydrate">hydrate(object)</h3>
940
979
  <p>
941
- Converts <code>$</code>-prefixed strings into reactive computed signals.
980
+ Converts <code>=/</code> prefixed strings into reactive computed signals.
942
981
  </p>
943
982
  <div class="code-block">
944
983
  <pre><code>const config = { title: "Dashboard", total: "=/cart/items...price" };