structscript 1.4.1 → 1.5.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/lib/editor.html CHANGED
@@ -714,147 +714,584 @@ code { font-family: 'DM Mono', monospace; font-size: 11px; color: var(--accent);
714
714
  </div><!-- playground -->
715
715
  </div><!-- page-playground -->
716
716
 
717
- <!-- DOCS PAGE -->
718
- <div id="page-docs" class="page">
719
- <div class="docs-page">
720
- <div class="docs-hero">
721
- <h2><em>StructScript</em> v1.1<br>Language Reference</h2>
722
- <p>A clean, readable language blending natural English with Python-style indentation. Built for beginners, scripters, and systems thinkers.</p>
723
- </div>
724
-
725
- <div class="docs-grid">
726
- <div class="docs-card"><h4>🧱 Structured</h4><p>Indentation-based blocks. No braces needed. Clean and readable at a glance.</p></div>
727
- <div class="docs-card"><h4>📖 Readable</h4><p>Keywords read like English: <code>say</code>, <code>define</code>, <code>count from to</code>, <code>repeat times</code>.</p></div>
728
- <div class="docs-card"><h4>⚡ Capable</h4><p>Functions, structs, lists, error handling, string ops, math all built in.</p></div>
729
- <div class="docs-card"><h4>🛡️ Safe</h4><p>Clear error messages with line numbers and code snippets. Stack overflow protection.</p></div>
730
- </div>
731
-
732
- <div class="docs-section">
733
- <h3>Variables & Constants</h3>
734
- <table class="docs-table">
735
- <tr><th>Syntax</th><th>Example</th><th>Notes</th></tr>
736
- <tr><td><code>let name = value</code></td><td><code>let x = 42</code></td><td>Mutable variable</td></tr>
737
- <tr><td><code>const name = value</code></td><td><code>const PI = 3.14159</code></td><td>Immutable constant</td></tr>
738
- <tr><td><code>set name = value</code></td><td><code>set x = x + 1</code></td><td>Reassign existing variable</td></tr>
739
- <tr><td><code>set obj.field = val</code></td><td><code>set person.age = 30</code></td><td>Struct field assignment</td></tr>
740
- </table>
741
- </div>
742
-
743
- <div class="docs-section">
744
- <h3>Types</h3>
745
- <table class="docs-table">
746
- <tr><th>Type</th><th>Examples</th><th>Notes</th></tr>
747
- <tr><td>Number</td><td><code>42</code>, <code>3.14</code>, <code>-7</code></td><td>Integer or float</td></tr>
748
- <tr><td>String</td><td><code>"hello"</code>, <code>'world'</code></td><td>Single or double quotes</td></tr>
749
- <tr><td>Boolean</td><td><code>true</code>, <code>false</code></td><td>Logical values</td></tr>
750
- <tr><td>Nothing</td><td><code>nothing</code></td><td>Null/None equivalent</td></tr>
751
- <tr><td>List</td><td><code>[1, 2, 3]</code></td><td>Ordered, mutable collection</td></tr>
752
- <tr><td>Struct</td><td><code>Point()</code></td><td>Custom data type</td></tr>
753
- </table>
754
- </div>
755
-
756
- <div class="docs-section">
757
- <h3>Operators</h3>
758
- <table class="docs-table">
759
- <tr><th>Category</th><th>Operators</th></tr>
760
- <tr><td>Arithmetic</td><td><code>+ - * / % ** //</code></td></tr>
761
- <tr><td>Comparison</td><td><code>== != &gt; &lt; &gt;= &lt;=</code></td></tr>
762
- <tr><td>Logical</td><td><code>and or not</code></td></tr>
763
- <tr><td>String</td><td><code>+</code> (concat), <code>*</code> (repeat)</td></tr>
764
- </table>
765
- </div>
766
-
767
- <div class="docs-section">
768
- <h3>Control Flow</h3>
769
- <div class="docs-code">
770
- <span class="kw">if</span> x > <span class="num">0</span>:
771
- <span class="kw">say</span> <span class="str">"positive"</span>
772
- <span class="kw">else if</span> x < <span class="num">0</span>:
773
- <span class="kw">say</span> <span class="str">"negative"</span>
774
- <span class="kw">else</span>:
775
- <span class="kw">say</span> <span class="str">"zero"</span>
776
-
777
- <span class="kw">while</span> x < <span class="num">100</span>:
778
- <span class="kw">set</span> x = x * <span class="num">2</span>
779
-
780
- <span class="kw">repeat</span> <span class="num">5</span> <span class="kw">times</span>:
781
- <span class="kw">say</span> <span class="str">"hello"</span>
782
-
783
- <span class="kw">count</span> i <span class="kw">from</span> <span class="num">1</span> <span class="kw">to</span> <span class="num">10</span>:
784
- <span class="kw">say</span> i
785
-
786
- <span class="kw">for each</span> item <span class="kw">in</span> myList:
787
- <span class="kw">say</span> item</div>
788
- </div>
789
-
790
- <div class="docs-section">
791
- <h3>Functions</h3>
792
- <div class="docs-code">
793
- <span class="kw">define</span> <span class="fn">greet</span>(name):
794
- <span class="kw">say</span> <span class="str">"Hello, "</span> + name
795
-
796
- <span class="kw">define</span> <span class="fn">add</span>(a, b):
797
- <span class="kw">return</span> a + b
798
-
799
- <span class="cmt">// Default parameters</span>
800
- <span class="kw">define</span> <span class="fn">power</span>(base, exp = <span class="num">2</span>):
801
- <span class="kw">return</span> base ** exp
802
-
803
- <span class="fn">greet</span>(<span class="str">"World"</span>)
804
- <span class="kw">let</span> result = <span class="fn">add</span>(<span class="num">3</span>, <span class="num">4</span>)</div>
805
- </div>
806
-
807
- <div class="docs-section">
808
- <h3>Error Handling</h3>
809
- <div class="docs-code">
810
- <span class="kw">try</span>:
811
- <span class="kw">let</span> x = <span class="num">10</span> / <span class="num">0</span>
812
- <span class="kw">catch</span> err:
813
- <span class="kw">say</span> <span class="str">"Caught: "</span> + err
814
-
815
- <span class="kw">try</span>:
816
- <span class="fn">riskyFunction</span>()
817
- <span class="kw">catch</span> e:
818
- <span class="kw">say</span> <span class="str">"Error: "</span> + e
819
- <span class="kw">finally</span>:
820
- <span class="kw">say</span> <span class="str">"Always runs"</span></div>
821
- </div>
822
-
823
- <div class="docs-section">
824
- <h3>Built-in Functions</h3>
825
- <table class="docs-table">
826
- <tr><th>Function</th><th>Description</th><th>Example</th></tr>
827
- <tr><td><code>say(x)</code></td><td>Print to output</td><td><code>say "hi"</code></td></tr>
828
- <tr><td><code>length(x)</code></td><td>Length of list/string</td><td><code>length([1,2,3])</code> → 3</td></tr>
829
- <tr><td><code>push(list, val)</code></td><td>Append to list</td><td><code>push(nums, 4)</code></td></tr>
830
- <tr><td><code>pop(list)</code></td><td>Remove last item</td><td><code>pop(nums)</code></td></tr>
831
- <tr><td><code>join(list, sep)</code></td><td>Join list to string</td><td><code>join(items, ", ")</code></td></tr>
832
- <tr><td><code>split(str, sep)</code></td><td>Split string to list</td><td><code>split("a,b", ",")</code></td></tr>
833
- <tr><td><code>upper(str)</code></td><td>Uppercase string</td><td><code>upper("hi")</code> → "HI"</td></tr>
834
- <tr><td><code>lower(str)</code></td><td>Lowercase string</td><td><code>lower("HI")</code> "hi"</td></tr>
835
- <tr><td><code>trim(str)</code></td><td>Strip whitespace</td><td><code>trim(" hi ")</code></td></tr>
836
- <tr><td><code>contains(str, sub)</code></td><td>Check substring</td><td><code>contains("hello", "ell")</code></td></tr>
837
- <tr><td><code>replace(str,old,new)</code></td><td>Replace in string</td><td><code>replace("hi","h","b")</code></td></tr>
838
- <tr><td><code>slice(list, a, b)</code></td><td>Sub-list or substring</td><td><code>slice(nums, 0, 3)</code></td></tr>
839
- <tr><td><code>sort(list)</code></td><td>Sort a list</td><td><code>sort([3,1,2])</code></td></tr>
840
- <tr><td><code>reverse(list)</code></td><td>Reverse a list</td><td><code>reverse([1,2,3])</code></td></tr>
841
- <tr><td><code>range(n)</code> / <code>range(a,b)</code></td><td>Generate number list</td><td><code>range(5)</code> → [0..4]</td></tr>
842
- <tr><td><code>abs(n)</code></td><td>Absolute value</td><td><code>abs(-5)</code> → 5</td></tr>
843
- <tr><td><code>sqrt(n)</code></td><td>Square root</td><td><code>sqrt(16)</code> → 4</td></tr>
844
- <tr><td><code>floor/ceil/round</code></td><td>Rounding</td><td><code>floor(3.7)</code> → 3</td></tr>
845
- <tr><td><code>max/min</code></td><td>Max/min of args</td><td><code>max(1,5,3)</code> → 5</td></tr>
846
- <tr><td><code>random()</code></td><td>Random 0–1 float</td><td><code>random()</code></td></tr>
847
- <tr><td><code>randint(a,b)</code></td><td>Random integer</td><td><code>randint(1,6)</code></td></tr>
848
- <tr><td><code>str(x)</code></td><td>Convert to string</td><td><code>str(42)</code> → "42"</td></tr>
849
- <tr><td><code>num(x)</code></td><td>Convert to number</td><td><code>num("42")</code> → 42</td></tr>
850
- <tr><td><code>bool(x)</code></td><td>Convert to boolean</td><td><code>bool(0)</code> → false</td></tr>
851
- <tr><td><code>type(x)</code></td><td>Get type name</td><td><code>type(42)</code> → "number"</td></tr>
852
- <tr><td><code>isNothing(x)</code></td><td>Check for nothing</td><td><code>isNothing(val)</code></td></tr>
853
- </table>
854
- </div>
717
+ <!-- DOCS PAGE -->
718
+ <div id="page-docs" class="page">
719
+ <div class="docs-page">
720
+
721
+ <div class="docs-hero">
722
+ <h2><em>StructScript</em> v1.4<br>Language Reference</h2>
723
+ <p>A clean, readable language with Python-style indentation. Script, process data, and build complete web pages — all in one syntax.</p>
724
+ </div>
725
+
726
+ <div class="docs-grid">
727
+ <div class="docs-card"><h4>🧱 Structured</h4><p>Indentation-based blocks. No braces. Reads like English.</p></div>
728
+ <div class="docs-card"><h4>📖 Readable</h4><p><code>say</code>, <code>define</code>, <code>count from to</code>, <code>repeat times</code>it just makes sense.</p></div>
729
+ <div class="docs-card"><h4>🌐 Web-Ready</h4><p>Every HTML tag, every CSS property, every browser event. <code>page</code> <code>add</code> → done.</p></div>
730
+ <div class="docs-card"><h4>⚡ Capable</h4><p>Classes, structs, error handling, 40+ built-ins, file imports.</p></div>
731
+ </div>
732
+
733
+ <div class="docs-section">
734
+ <h3>Variables &amp; Constants</h3>
735
+ <table class="docs-table">
736
+ <tr><th>Syntax</th><th>Example</th><th>Notes</th></tr>
737
+ <tr><td><code>let name = value</code></td><td><code>let x = 42</code></td><td>Mutable variable</td></tr>
738
+ <tr><td><code>const name = value</code></td><td><code>const PI = 3.14159</code></td><td>Immutable constant</td></tr>
739
+ <tr><td><code>set name = value</code></td><td><code>set x = x + 1</code></td><td>Reassign</td></tr>
740
+ <tr><td><code>set name += val</code></td><td><code>set x += 5</code></td><td>+= -= *= /= %= **= //=</td></tr>
741
+ <tr><td><code>set obj.field = val</code></td><td><code>set p.age = 30</code></td><td>Object field</td></tr>
742
+ </table>
743
+ </div>
744
+
745
+ <div class="docs-section">
746
+ <h3>Types</h3>
747
+ <table class="docs-table">
748
+ <tr><th>Type</th><th>Examples</th><th>Notes</th></tr>
749
+ <tr><td>Number</td><td><code>42</code>, <code>3.14</code>, <code>-7</code></td><td>Integer or float</td></tr>
750
+ <tr><td>String</td><td><code>"hello {name}!"</code></td><td>Interpolation with { }</td></tr>
751
+ <tr><td>Boolean</td><td><code>true</code>, <code>false</code></td><td></td></tr>
752
+ <tr><td>Nothing</td><td><code>nothing</code></td><td>Null/None equivalent</td></tr>
753
+ <tr><td>List</td><td><code>[1, 2, 3]</code></td><td>Ordered, mutable</td></tr>
754
+ <tr><td>Struct</td><td><code>Point()</code></td><td>Custom data type</td></tr>
755
+ <tr><td>Class</td><td><code>Dog()</code></td><td>Methods + <code>self</code></td></tr>
756
+ </table>
757
+ </div>
758
+
759
+ <div class="docs-section">
760
+ <h3>Operators</h3>
761
+ <table class="docs-table">
762
+ <tr><th>Category</th><th>Operators</th><th>Notes</th></tr>
763
+ <tr><td>Arithmetic</td><td><code>+ - * / % ** //</code></td><td>// = floor division</td></tr>
764
+ <tr><td>Comparison</td><td><code>== != &gt; &lt; &gt;= &lt;=</code></td><td></td></tr>
765
+ <tr><td>Logical</td><td><code>and or not</code></td><td></td></tr>
766
+ <tr><td>Ternary</td><td><code>a if cond else b</code></td><td>Inline conditional</td></tr>
767
+ </table>
768
+ </div>
769
+
770
+ <div class="docs-section">
771
+ <h3>Control Flow</h3>
772
+ <div class="docs-code">
773
+ <span class="kw">if</span> x > <span class="num">0</span>:
774
+ <span class="kw">say</span> <span class="str">"positive"</span>
775
+ <span class="kw">else if</span> x < <span class="num">0</span>:
776
+ <span class="kw">say</span> <span class="str">"negative"</span>
777
+ <span class="kw">else</span>:
778
+ <span class="kw">say</span> <span class="str">"zero"</span>
779
+
780
+ <span class="kw">while</span> x < <span class="num">100</span>:
781
+ <span class="kw">set</span> x *= <span class="num">2</span>
782
+
783
+ <span class="kw">repeat</span> <span class="num">5</span> <span class="kw">times</span>:
784
+ <span class="kw">say</span> <span class="str">"hello"</span>
785
+
786
+ <span class="kw">count</span> i <span class="kw">from</span> <span class="num">1</span> <span class="kw">to</span> <span class="num">10</span> <span class="kw">step</span> <span class="num">2</span>:
787
+ <span class="kw">say</span> i
788
+
789
+ <span class="kw">for each</span> item <span class="kw">in</span> myList:
790
+ <span class="kw">say</span> item</div>
791
+ </div>
792
+
793
+ <div class="docs-section">
794
+ <h3>Functions</h3>
795
+ <div class="docs-code">
796
+ <span class="kw">define</span> <span class="fn">greet</span>(name, msg = <span class="str">"Hello"</span>):
797
+ <span class="kw">say</span> <span class="str">"{msg}, {name}!"</span>
798
+
799
+ <span class="kw">define</span> <span class="fn">fib</span>(n):
800
+ <span class="kw">if</span> n <= <span class="num">1</span>: <span class="kw">return</span> n
801
+ <span class="kw">return</span> <span class="fn">fib</span>(n - <span class="num">1</span>) + <span class="fn">fib</span>(n - <span class="num">2</span>)</div>
802
+ </div>
803
+
804
+ <div class="docs-section">
805
+ <h3>Classes &amp; Structs</h3>
806
+ <div class="docs-code">
807
+ <span class="kw">class</span> Animal:
808
+ name = <span class="str">""</span>
809
+ <span class="kw">define</span> <span class="fn">init</span>(n):
810
+ <span class="kw">set</span> self.name = n
811
+ <span class="kw">define</span> <span class="fn">speak</span>():
812
+ <span class="kw">say</span> <span class="str">"{self.name} says hi!"</span>
813
+
814
+ <span class="kw">let</span> dog = <span class="fn">Animal</span>()
815
+ dog.<span class="fn">init</span>(<span class="str">"Rex"</span>)
816
+ dog.<span class="fn">speak</span>()
817
+
818
+ <span class="cmt">// Struct (lightweight data container)</span>
819
+ <span class="kw">struct</span> Point:
820
+ x = <span class="num">0</span>
821
+ y = <span class="num">0</span>
822
+ <span class="kw">let</span> p = <span class="fn">Point</span>()
823
+ <span class="kw">set</span> p.x = <span class="num">10</span></div>
824
+ </div>
825
+
826
+ <div class="docs-section">
827
+ <h3>Error Handling</h3>
828
+ <div class="docs-code">
829
+ <span class="kw">try</span>:
830
+ <span class="fn">riskyFunction</span>()
831
+ <span class="kw">catch</span> e:
832
+ <span class="kw">say</span> <span class="str">"Error: {e}"</span>
833
+ <span class="kw">finally</span>:
834
+ <span class="kw">say</span> <span class="str">"Always runs"</span></div>
835
+ </div>
836
+
837
+ <div class="docs-section">
838
+ <h3>Built-in Functions</h3>
839
+ <table class="docs-table">
840
+ <tr><th>Function</th><th>Description</th><th>Example</th></tr>
841
+ <tr><td><code>say(x)</code></td><td>Print output</td><td><code>say "hi"</code></td></tr>
842
+ <tr><td><code>input(prompt)</code></td><td>Read user input</td><td><code>let n = input("Name?")</code></td></tr>
843
+ <tr><td><code>length(x)</code></td><td>String or list length</td><td><code>length([1,2,3])</code> → 3</td></tr>
844
+ <tr><td><code>push / pop / shift</code></td><td>List mutation</td><td><code>push(nums, 4)</code></td></tr>
845
+ <tr><td><code>join(list, sep)</code></td><td>List string</td><td><code>join(items, ", ")</code></td></tr>
846
+ <tr><td><code>split(str, sep)</code></td><td>String list</td><td><code>split("a,b", ",")</code></td></tr>
847
+ <tr><td><code>upper / lower / trim</code></td><td>String transform</td><td><code>upper("hi")</code> → "HI"</td></tr>
848
+ <tr><td><code>replace(s, old, new)</code></td><td>Replace substring</td><td><code>replace("hi","h","b")</code></td></tr>
849
+ <tr><td><code>contains(s, sub)</code></td><td>Substring check</td><td><code>contains("hello","ell")</code></td></tr>
850
+ <tr><td><code>slice(x, a, b)</code></td><td>Sub-list or substring</td><td><code>slice(nums, 0, 3)</code></td></tr>
851
+ <tr><td><code>sort / reverse</code></td><td>List order</td><td><code>sort([3,1,2])</code></td></tr>
852
+ <tr><td><code>map / filter / reduce</code></td><td>List transforms</td><td><code>map(nums, double)</code></td></tr>
853
+ <tr><td><code>sum / avg / unique</code></td><td>List aggregation</td><td><code>sum([1,2,3])</code> → 6</td></tr>
854
+ <tr><td><code>range(a, b)</code></td><td>Number list</td><td><code>range(0, 5)</code> → [0..4]</td></tr>
855
+ <tr><td><code>abs / sqrt / floor / ceil</code></td><td>Math</td><td><code>sqrt(16)</code> → 4</td></tr>
856
+ <tr><td><code>max / min</code></td><td>Largest / smallest</td><td><code>max(1,5,3)</code> → 5</td></tr>
857
+ <tr><td><code>random()</code></td><td>Float 0–1</td><td><code>random()</code></td></tr>
858
+ <tr><td><code>randint(a, b)</code></td><td>Random integer</td><td><code>randint(1,6)</code></td></tr>
859
+ <tr><td><code>str / num / bool / type</code></td><td>Type conversion</td><td><code>num("42")</code> → 42</td></tr>
860
+ </table>
861
+ </div>
862
+
863
+ <!-- ═══ WEB MODE ════════════════════════════════════════════ -->
864
+ <div class="docs-section" style="border-top:2px solid var(--lime);margin-top:40px;padding-top:32px">
865
+ <h3 style="color:var(--lime)">🌐 Web Mode — Build Real Pages</h3>
866
+ <p style="color:var(--ink2);margin-bottom:20px">Any file starting with <code>page</code> or <code>add</code> enters <strong>Web Mode</strong>. StructScript compiles to a complete, standalone HTML file with full CSS and JavaScript support. Every HTML element, CSS property, pseudo-class, media query, and browser event is supported.</p>
867
+
868
+ <h4 style="margin:0 0 10px">Page Setup</h4>
869
+ <div class="docs-code">
870
+ <span class="kw">page</span> <span class="str">"My Site"</span>:
871
+ <span class="cmt">// Body styles</span>
872
+ style background <span class="str">"#0d1f1e"</span>
873
+ style fontFamily <span class="str">"Manrope, sans-serif"</span>
874
+ style color <span class="str">"#e0f0ee"</span>
875
+ <span class="cmt">// &lt;head&gt; metadata</span>
876
+ viewport <span class="str">"width=device-width, initial-scale=1"</span>
877
+ meta description <span class="str">"My awesome page"</span>
878
+ meta author <span class="str">"You"</span>
879
+ favicon <span class="str">"/icon.ico"</span>
880
+ font <span class="str">"Manrope"</span>
881
+ <span class="cmt">// External resources</span>
882
+ link <span class="str">'rel="stylesheet" href="styles.css"'</span>
883
+ script src <span class="str">"https://cdn.jsdelivr.net/npm/chart.js"</span>
884
+ <span class="cmt">// Open Graph</span>
885
+ og:title <span class="str">"My Site"</span>
886
+ og:image <span class="str">"https://example.com/thumb.png"</span>
887
+ canonical <span class="str">"https://example.com"</span>
888
+ lang <span class="str">"en"</span></div>
889
+
890
+ <h4 style="margin:20px 0 10px">Elements — Every HTML Tag</h4>
891
+ <p style="color:var(--ink2);margin-bottom:10px">Use <code>add TAG</code> for any HTML element. Nesting follows indentation.</p>
892
+ <div class="docs-code">
893
+ <span class="cmt">// Selector formats:</span>
894
+ <span class="kw">add</span> div <span class="str">"myId"</span>: <span class="cmt">// → &lt;div id="myId"&gt;</span>
895
+ <span class="kw">add</span> div <span class="str">".card"</span>: <span class="cmt">// → &lt;div class="card"&gt;</span>
896
+ <span class="kw">add</span> div <span class="str">"#hero .big"</span>: <span class="cmt">// → &lt;div id="hero" class="big"&gt;</span>
897
+ <span class="kw">add</span> div: <span class="cmt">// → &lt;div&gt; (no id/class)</span>
898
+
899
+ <span class="cmt">// Semantic HTML5 structure:</span>
900
+ <span class="kw">add</span> header <span class="str">""</span>:
901
+ <span class="kw">add</span> nav <span class="str">""</span>:
902
+ <span class="kw">add</span> a <span class="str">""</span>: text <span class="str">"Home"</span> href <span class="str">"/"</span>
903
+ <span class="kw">add</span> main <span class="str">""</span>:
904
+ <span class="kw">add</span> section <span class="str">"#about"</span>:
905
+ <span class="kw">add</span> article <span class="str">""</span>:
906
+ <span class="kw">add</span> h1 <span class="str">""</span>: text <span class="str">"About Me"</span>
907
+ <span class="kw">add</span> p <span class="str">""</span>: text <span class="str">"Developer &amp; maker."</span>
908
+ <span class="kw">add</span> aside <span class="str">""</span>:
909
+ <span class="kw">add</span> p <span class="str">""</span>: text <span class="str">"Sidebar content"</span>
910
+ <span class="kw">add</span> footer <span class="str">""</span>:
911
+ <span class="kw">add</span> p <span class="str">""</span>: text <span class="str">"© 2025"</span></div>
912
+
913
+ <h4 style="margin:20px 0 10px">Content &amp; Common Attributes</h4>
914
+ <table class="docs-table">
915
+ <tr><th>Command</th><th>HTML equivalent</th></tr>
916
+ <tr><td><code>text "hello"</code></td><td>Inner text content</td></tr>
917
+ <tr><td><code>html "&lt;b&gt;bold&lt;/b&gt;"</code></td><td>Raw inner HTML</td></tr>
918
+ <tr><td><code>src "url"</code></td><td><code>src="url"</code> — img, video, audio, iframe, script</td></tr>
919
+ <tr><td><code>href "url"</code></td><td><code>href="url"</code> — a, link</td></tr>
920
+ <tr><td><code>alt "text"</code></td><td><code>alt="text"</code></td></tr>
921
+ <tr><td><code>title "text"</code></td><td><code>title="text"</code> tooltip</td></tr>
922
+ <tr><td><code>type "email"</code></td><td><code>type="email"</code> — text email password number tel url date range color file</td></tr>
923
+ <tr><td><code>name "field"</code></td><td><code>name="field"</code></td></tr>
924
+ <tr><td><code>value "x"</code></td><td><code>value="x"</code></td></tr>
925
+ <tr><td><code>placeholder "hint"</code></td><td><code>placeholder="hint"</code></td></tr>
926
+ <tr><td><code>for "id"</code></td><td><code>for="id"</code> — label</td></tr>
927
+ <tr><td><code>action "url"</code></td><td>form action</td></tr>
928
+ <tr><td><code>method "post"</code></td><td>form method</td></tr>
929
+ <tr><td><code>target "_blank"</code></td><td>link/form target</td></tr>
930
+ <tr><td><code>rel "noopener"</code></td><td>link rel</td></tr>
931
+ <tr><td><code>width "300"</code> / <code>height "200"</code></td><td>Element dimensions</td></tr>
932
+ <tr><td><code>rows "5"</code> / <code>cols "40"</code></td><td>textarea size</td></tr>
933
+ <tr><td><code>min "0"</code> / <code>max "100"</code> / <code>step "1"</code></td><td>number/range input</td></tr>
934
+ <tr><td><code>accept ".jpg,.png"</code></td><td>file input accept</td></tr>
935
+ <tr><td><code>pattern "[A-Z]+"</code></td><td>input validation regex</td></tr>
936
+ <tr><td><code>maxlength "100"</code></td><td>max character count</td></tr>
937
+ <tr><td><code>srcset "img.png 2x"</code></td><td>responsive images</td></tr>
938
+ <tr><td><code>loading "lazy"</code></td><td>lazy-load images/iframes</td></tr>
939
+ <tr><td><code>poster "thumb.jpg"</code></td><td>video poster frame</td></tr>
940
+ <tr><td><code>sandbox "allow-scripts"</code></td><td>iframe sandbox</td></tr>
941
+ <tr><td><code>colspan "2"</code> / <code>rowspan "3"</code></td><td>table cell spanning</td></tr>
942
+ <tr><td><code>role "button"</code></td><td>ARIA role</td></tr>
943
+ <tr><td><code>tabindex "0"</code></td><td>keyboard tab order</td></tr>
944
+ <tr><td><code>aria-label "Close"</code></td><td>any <code>aria-*</code> attribute</td></tr>
945
+ <tr><td><code>data-id "123"</code></td><td>any <code>data-*</code> attribute</td></tr>
946
+ <tr><td><code>class "extra"</code></td><td>add CSS class(es)</td></tr>
947
+ <tr><td><code>attr NAME VALUE</code></td><td>any other HTML attribute</td></tr>
948
+ </table>
949
+ <p style="margin-top:8px;color:var(--ink2);font-size:12px">Boolean attributes (no value): <code>checked disabled readonly required autoplay loop controls muted multiple autofocus hidden open selected novalidate async defer reversed ismap allowfullscreen</code></p>
950
+
951
+ <h4 style="margin:20px 0 10px">All Styling — CSS Properties</h4>
952
+ <p style="color:var(--ink2);margin-bottom:10px">Every CSS property works. Use camelCase or kebab-case — both accepted.</p>
953
+ <div class="docs-code">
954
+ <span class="kw">add</span> div <span class="str">".card"</span>:
955
+ <span class="cmt">// Any CSS property via style</span>
956
+ style display <span class="str">"flex"</span>
957
+ style flexDirection <span class="str">"column"</span> <span class="cmt">// or: flex-direction</span>
958
+ style backgroundColor <span class="str">"#162b28"</span>
959
+ style borderRadius <span class="str">"12px"</span>
960
+ style padding <span class="str">"24px"</span>
961
+ style boxShadow <span class="str">"0 4px 24px rgba(0,0,0,0.4)"</span>
962
+ style background <span class="str">"linear-gradient(135deg,#0b7a75,#162b28)"</span>
963
+ style backgroundImage <span class="str">"url('/hero.jpg')"</span>
964
+ style backgroundSize <span class="str">"cover"</span>
965
+ style backdropFilter <span class="str">"blur(12px)"</span>
966
+ style webkitBackdropFilter <span class="str">"blur(12px)"</span>
967
+ style clipPath <span class="str">"polygon(0 0,100% 0,100% 85%,0 100%)"</span>
968
+ <span class="cmt">// Shorthand helpers</span>
969
+ transition <span class="str">"all 0.25s ease"</span>
970
+ transform <span class="str">"rotate(45deg) scale(1.1)"</span>
971
+ filter <span class="str">"drop-shadow(0 4px 8px rgba(0,0,0,0.5))"</span>
972
+ flex <span class="str">"1 0 200px"</span>
973
+ grid <span class="str">"repeat(3, 1fr)"</span>
974
+ gap <span class="str">"16px"</span>
975
+ bg <span class="str">"#0b7a75"</span>
976
+ shadow <span class="str">"0 8px 32px rgba(0,0,0,0.3)"</span>
977
+ cursor <span class="str">"pointer"</span>
978
+ opacity <span class="str">"0.8"</span>
979
+ zindex <span class="str">"100"</span>
980
+ outline <span class="str">"2px solid #b8f000"</span>
981
+ overflow <span class="str">"hidden"</span>
982
+ <span class="cmt">// CSS custom properties</span>
983
+ var --brand <span class="str">"#b8f000"</span></div>
984
+
985
+ <h4 style="margin:20px 0 10px">Pseudo-classes &amp; States</h4>
986
+ <p style="color:var(--ink2);margin-bottom:10px">All CSS pseudo-classes are supported using the same syntax: <code>PSEUDO property value</code>.</p>
987
+ <table class="docs-table">
988
+ <tr><th>Command</th><th>CSS Pseudo-class</th><th>Example</th></tr>
989
+ <tr><td><code>hover PROP VAL</code></td><td><code>:hover</code></td><td><code>hover background "#12a89e"</code></td></tr>
990
+ <tr><td><code>focus PROP VAL</code></td><td><code>:focus</code></td><td><code>focus outline "2px solid #b8f000"</code></td></tr>
991
+ <tr><td><code>active PROP VAL</code></td><td><code>:active</code></td><td><code>active transform "scale(0.97)"</code></td></tr>
992
+ <tr><td><code>visited PROP VAL</code></td><td><code>:visited</code></td><td><code>visited color "#8ab8b4"</code></td></tr>
993
+ <tr><td><code>checked PROP VAL</code></td><td><code>:checked</code></td><td><code>checked background "#b8f000"</code></td></tr>
994
+ <tr><td><code>disabled PROP VAL</code></td><td><code>:disabled</code></td><td><code>disabled opacity "0.4"</code></td></tr>
995
+ <tr><td><code>focus-visible PROP VAL</code></td><td><code>:focus-visible</code></td><td><code>focus-visible outline "3px solid #b8f000"</code></td></tr>
996
+ <tr><td><code>placeholder PROP VAL</code></td><td><code>::placeholder</code></td><td><code>placeholder color "#4a7470"</code></td></tr>
997
+ <tr><td><code>first-child PROP VAL</code></td><td><code>:first-child</code></td><td><code>first-child marginTop "0"</code></td></tr>
998
+ <tr><td><code>last-child PROP VAL</code></td><td><code>:last-child</code></td><td><code>last-child marginBottom "0"</code></td></tr>
999
+ <tr><td><code>invalid PROP VAL</code></td><td><code>:invalid</code></td><td><code>invalid borderColor "red"</code></td></tr>
1000
+ <tr><td><code>valid PROP VAL</code></td><td><code>:valid</code></td><td><code>valid borderColor "#b8f000"</code></td></tr>
1001
+ </table>
1002
+
1003
+ <h4 style="margin:20px 0 10px">Pseudo-elements</h4>
1004
+ <div class="docs-code">
1005
+ <span class="kw">add</span> li <span class="str">".item"</span>:
1006
+ text <span class="str">"List item"</span>
1007
+ before <span class="str">"→ "</span> <span class="cmt">// injects ::before { content: "→ " }</span>
1008
+ after <span class="str">" ✓"</span> <span class="cmt">// injects ::after { content: " ✓" }</span></div>
1009
+
1010
+ <h4 style="margin:20px 0 10px">Responsive Design — Media Queries</h4>
1011
+ <div class="docs-code">
1012
+ <span class="kw">add</span> div <span class="str">"#grid"</span>:
1013
+ grid <span class="str">"repeat(3, 1fr)"</span>
1014
+ gap <span class="str">"24px"</span>
1015
+ media <span class="str">"max-width:900px"</span> gridTemplateColumns <span class="str">"repeat(2,1fr)"</span>
1016
+ media <span class="str">"max-width:600px"</span> gridTemplateColumns <span class="str">"1fr"</span>
1017
+ media <span class="str">"max-width:600px"</span> gap <span class="str">"12px"</span>
1018
+
1019
+ <span class="cmt">// Full @media / @container / @supports via the css: block</span>
1020
+ <span class="kw">css</span>:
1021
+ @media (prefers-color-scheme: dark) { body { background: #000 } }
1022
+ @container sidebar (min-width: 300px) { .widget { flex-direction: row } }
1023
+ @supports (backdrop-filter: blur(1px)) { .glass { backdrop-filter: blur(12px) } }</div>
1024
+
1025
+ <h4 style="margin:20px 0 10px">Events — Every Browser Event</h4>
1026
+ <p style="color:var(--ink2);margin-bottom:10px">Write plain JavaScript in the handler body. The <code>event</code> object and <code>this</code> (the element) are available.</p>
1027
+ <div class="docs-code">
1028
+ <span class="kw">add</span> button <span class="str">"btn"</span>:
1029
+ text <span class="str">"Save"</span>
1030
+ <span class="kw">on</span> click:
1031
+ const data = document.getElementById('editor').value;
1032
+ localStorage.setItem('draft', data);
1033
+ this.textContent = 'Saved!';
1034
+
1035
+ <span class="kw">add</span> input <span class="str">"search"</span>:
1036
+ type <span class="str">"text"</span>
1037
+ placeholder <span class="str">"Search..."</span>
1038
+ <span class="kw">on</span> input:
1039
+ filterResults(event.target.value)
1040
+ <span class="kw">on</span> keydown:
1041
+ if (event.key === 'Enter') submitSearch();
1042
+
1043
+ <span class="kw">add</span> div <span class="str">"dropzone"</span>:
1044
+ <span class="kw">on</span> dragover:
1045
+ event.preventDefault();
1046
+ this.style.background = '#1e3530';
1047
+ <span class="kw">on</span> drop:
1048
+ event.preventDefault();
1049
+ const file = event.dataTransfer.files[0];
1050
+ handleFile(file);</div>
1051
+ <p style="margin-top:8px;color:var(--ink2);font-size:12px">All events: <code>click dblclick contextmenu mousedown mouseup mouseover mouseout mouseenter mouseleave mousemove keydown keyup keypress input change focus blur submit reset select scroll resize wheel drag drop touch pointer animationend transitionend load error play pause ended copy paste</code> and more.</p>
1052
+
1053
+ <h4 style="margin:20px 0 10px">Animations</h4>
1054
+ <table class="docs-table">
1055
+ <tr><th>Command</th><th>Effect</th><th>Loop?</th></tr>
1056
+ <tr><td><code>animate fadeIn 0.5</code></td><td>Fade + rise from below</td><td>No</td></tr>
1057
+ <tr><td><code>animate fadeOut 0.3</code></td><td>Fade out</td><td>No</td></tr>
1058
+ <tr><td><code>animate slideIn 0.4</code></td><td>Slide from left</td><td>No</td></tr>
1059
+ <tr><td><code>animate slideInRight 0.4</code></td><td>Slide from right</td><td>No</td></tr>
1060
+ <tr><td><code>animate slideUp 0.4</code></td><td>Rise from below</td><td>No</td></tr>
1061
+ <tr><td><code>animate slideDown 0.4</code></td><td>Drop from above</td><td>No</td></tr>
1062
+ <tr><td><code>animate pop 0.4</code></td><td>Scale pop-in</td><td>No</td></tr>
1063
+ <tr><td><code>animate flip 0.5</code></td><td>3D Y-axis flip</td><td>No</td></tr>
1064
+ <tr><td><code>animate zoom 0.4</code></td><td>Scale from zero</td><td>No</td></tr>
1065
+ <tr><td><code>animate typewriter 2</code></td><td>Text reveal left→right</td><td>No</td></tr>
1066
+ <tr><td><code>animate bounce 0.6</code></td><td>Vertical bounce</td><td>∞</td></tr>
1067
+ <tr><td><code>animate spin 1</code></td><td>Continuous rotation</td><td>∞</td></tr>
1068
+ <tr><td><code>animate pulse 1.5</code></td><td>Opacity pulse</td><td>∞</td></tr>
1069
+ <tr><td><code>animate shake 0.4</code></td><td>Horizontal shake</td><td>No</td></tr>
1070
+ <tr><td><code>animate wiggle 0.4</code></td><td>Rotation wiggle</td><td>No</td></tr>
1071
+ </table>
1072
+ <p style="margin-top:8px;color:var(--ink2);font-size:12px">Optional third argument: easing — <code>ease ease-in ease-out ease-in-out linear cubic-bezier(…)</code></p>
1073
+
1074
+ <h4 style="margin:20px 0 10px">Raw CSS &amp; JavaScript Blocks</h4>
1075
+ <div class="docs-code">
1076
+ <span class="cmt">// Inject anything into &lt;style&gt;</span>
1077
+ <span class="kw">css</span>:
1078
+ :root { --brand: #b8f000; --bg: #0d1f1e; }
1079
+ @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;800&display=swap');
1080
+ ::selection { background: #b8f000; color: #0d1f1e; }
1081
+ ::-webkit-scrollbar { width: 6px }
1082
+ ::-webkit-scrollbar-thumb { background: #234440; border-radius: 3px }
1083
+ html { scroll-behavior: smooth }
1084
+ @keyframes myCustomAnim { 0% { opacity:0 } 100% { opacity:1 } }
1085
+ @media print { nav { display: none } }
1086
+
1087
+ <span class="cmt">// Inject JavaScript — runs after DOM is ready</span>
1088
+ <span class="kw">script</span>:
1089
+ const cards = document.querySelectorAll('.card');
1090
+ cards.forEach((card, i) => {
1091
+ card.style.animationDelay = (i * 0.1) + 's';
1092
+ });
1093
+
1094
+ function filterResults(query) {
1095
+ cards.forEach(c => {
1096
+ c.style.display = c.textContent.includes(query) ? '' : 'none';
1097
+ });
1098
+ }</div>
1099
+
1100
+ <h4 style="margin:20px 0 10px">Forms, Tables, Media</h4>
1101
+ <div class="docs-code">
1102
+ <span class="cmt">// Form with validation</span>
1103
+ <span class="kw">add</span> form <span class="str">"#contact"</span>:
1104
+ action <span class="str">"/submit"</span>
1105
+ method <span class="str">"post"</span>
1106
+ <span class="kw">on</span> submit:
1107
+ event.preventDefault();
1108
+ const fd = new FormData(this);
1109
+ fetch('/api', {method:'POST', body:fd});
1110
+
1111
+ <span class="kw">add</span> label <span class="str">""</span>: text <span class="str">"Email"</span> for <span class="str">"email"</span>
1112
+ <span class="kw">add</span> input <span class="str">"email"</span>:
1113
+ type <span class="str">"email"</span> name <span class="str">"email"</span> required
1114
+ placeholder <span class="str">"you@example.com"</span>
1115
+ pattern <span class="str">"[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}"</span>
1116
+ focus outline <span class="str">"2px solid #b8f000"</span>
1117
+ valid borderColor <span class="str">"#b8f000"</span>
1118
+ invalid borderColor <span class="str">"#f07070"</span>
1119
+
1120
+ <span class="kw">add</span> select <span class="str">"topic"</span>:
1121
+ name <span class="str">"topic"</span>
1122
+ <span class="kw">add</span> option <span class="str">""</span>: text <span class="str">"General"</span> value <span class="str">"general"</span>
1123
+ <span class="kw">add</span> option <span class="str">""</span>: text <span class="str">"Support"</span> value <span class="str">"support"</span> selected
1124
+
1125
+ <span class="cmt">// Responsive table</span>
1126
+ <span class="kw">add</span> table <span class="str">""</span>:
1127
+ style width <span class="str">"100%"</span>
1128
+ style borderCollapse <span class="str">"collapse"</span>
1129
+ <span class="kw">add</span> thead <span class="str">""</span>:
1130
+ <span class="kw">add</span> tr <span class="str">""</span>:
1131
+ <span class="kw">add</span> th <span class="str">""</span>: text <span class="str">"Name"</span> style padding <span class="str">"12px"</span>
1132
+ <span class="kw">add</span> th <span class="str">""</span>: text <span class="str">"Score"</span> colspan <span class="str">"2"</span>
1133
+ <span class="kw">add</span> tbody <span class="str">""</span>:
1134
+ <span class="kw">add</span> tr <span class="str">""</span>:
1135
+ <span class="kw">add</span> td <span class="str">""</span>: text <span class="str">"Alice"</span>
1136
+ <span class="kw">add</span> td <span class="str">""</span>: text <span class="str">"95"</span>
1137
+
1138
+ <span class="cmt">// Video with controls</span>
1139
+ <span class="kw">add</span> video <span class="str">"player"</span>:
1140
+ src <span class="str">"movie.mp4"</span>
1141
+ poster <span class="str">"thumb.jpg"</span>
1142
+ controls
1143
+ loop
1144
+ style width <span class="str">"100%"</span>
1145
+ style borderRadius <span class="str">"12px"</span>
1146
+ <span class="kw">on</span> play:
1147
+ console.log('Video started')
1148
+
1149
+ <span class="cmt">// Canvas</span>
1150
+ <span class="kw">add</span> canvas <span class="str">"myCanvas"</span>:
1151
+ width <span class="str">"800"</span> height <span class="str">"400"</span>
1152
+ style border <span class="str">"1px solid #234440"</span>
1153
+
1154
+ <span class="kw">script</span>:
1155
+ const ctx = document.getElementById('myCanvas').getContext('2d');
1156
+ ctx.fillStyle = '#b8f000';
1157
+ ctx.fillRect(10, 10, 200, 100);</div>
1158
+
1159
+ <h4 style="margin:20px 0 10px">Complete Example — Landing Page</h4>
1160
+ <div class="docs-code">
1161
+ <span class="kw">page</span> <span class="str">"Dev Portfolio"</span>:
1162
+ style background <span class="str">"#0a1614"</span>
1163
+ style color <span class="str">"#e0f0ee"</span>
1164
+ style fontFamily <span class="str">"system-ui, sans-serif"</span>
1165
+ style lineHeight <span class="str">"1.6"</span>
1166
+ meta description <span class="str">"Portfolio"</span>
1167
+ font <span class="str">"Manrope"</span>
1168
+
1169
+ <span class="kw">add</span> nav <span class="str">"#nav"</span>:
1170
+ style position <span class="str">"fixed"</span>
1171
+ style top <span class="str">"0"</span> style left <span class="str">"0"</span> style right <span class="str">"0"</span>
1172
+ style display <span class="str">"flex"</span>
1173
+ style justifyContent <span class="str">"space-between"</span>
1174
+ style alignItems <span class="str">"center"</span>
1175
+ style padding <span class="str">"0 40px"</span>
1176
+ style height <span class="str">"60px"</span>
1177
+ style background <span class="str">"rgba(10,22,20,0.9)"</span>
1178
+ style backdropFilter <span class="str">"blur(10px)"</span>
1179
+ style zIndex <span class="str">"1000"</span>
1180
+ style borderBottom <span class="str">"1px solid #234440"</span>
1181
+
1182
+ <span class="kw">add</span> span <span class="str">""</span>:
1183
+ text <span class="str">"MyPortfolio"</span>
1184
+ style fontWeight <span class="str">"800"</span>
1185
+ style color <span class="str">"#b8f000"</span>
1186
+ style letterSpacing <span class="str">"-0.5px"</span>
1187
+
1188
+ <span class="kw">add</span> div <span class="str">".nav-links"</span>:
1189
+ style display <span class="str">"flex"</span>
1190
+ style gap <span class="str">"28px"</span>
1191
+ <span class="kw">add</span> a <span class="str">""</span>: text <span class="str">"Work"</span> href <span class="str">"#work"</span> style color <span class="str">"#8ab8b4"</span> hover color <span class="str">"#b8f000"</span> transition <span class="str">"color 0.2s"</span> style textDecoration <span class="str">"none"</span>
1192
+ <span class="kw">add</span> a <span class="str">""</span>: text <span class="str">"About"</span> href <span class="str">"#about"</span> style color <span class="str">"#8ab8b4"</span> hover color <span class="str">"#b8f000"</span> transition <span class="str">"color 0.2s"</span> style textDecoration <span class="str">"none"</span>
1193
+
1194
+ <span class="kw">add</span> section <span class="str">"#hero"</span>:
1195
+ style minHeight <span class="str">"100vh"</span>
1196
+ style display <span class="str">"flex"</span>
1197
+ style flexDirection <span class="str">"column"</span>
1198
+ style justifyContent <span class="str">"center"</span>
1199
+ style alignItems <span class="str">"center"</span>
1200
+ style textAlign <span class="str">"center"</span>
1201
+ style padding <span class="str">"80px 24px 40px"</span>
1202
+ <span class="kw">add</span> h1 <span class="str">""</span>:
1203
+ text <span class="str">"I build things for the web"</span>
1204
+ style fontSize <span class="str">"clamp(2.5rem,7vw,5rem)"</span>
1205
+ style fontWeight <span class="str">"800"</span>
1206
+ style lineHeight <span class="str">"1.1"</span>
1207
+ style letterSpacing <span class="str">"-2px"</span>
1208
+ animate fadeIn 1
1209
+ <span class="kw">add</span> p <span class="str">""</span>:
1210
+ text <span class="str">"Full-stack developer. Open source contributor."</span>
1211
+ style color <span class="str">"#8ab8b4"</span>
1212
+ style fontSize <span class="str">"1.2rem"</span>
1213
+ style marginTop <span class="str">"16px"</span>
1214
+ animate fadeIn 1.2
1215
+ <span class="kw">add</span> a <span class="str">".cta"</span>:
1216
+ text <span class="str">"View my work ↓"</span>
1217
+ href <span class="str">"#work"</span>
1218
+ style display <span class="str">"inline-block"</span>
1219
+ style marginTop <span class="str">"40px"</span>
1220
+ style background <span class="str">"#b8f000"</span>
1221
+ style color <span class="str">"#0a1614"</span>
1222
+ style padding <span class="str">"14px 36px"</span>
1223
+ style borderRadius <span class="str">"50px"</span>
1224
+ style fontWeight <span class="str">"700"</span>
1225
+ style textDecoration <span class="str">"none"</span>
1226
+ style fontSize <span class="str">"1rem"</span>
1227
+ hover background <span class="str">"#d4ff33"</span>
1228
+ hover transform <span class="str">"translateY(-2px)"</span>
1229
+ active transform <span class="str">"translateY(0)"</span>
1230
+ transition <span class="str">"all 0.2s"</span>
1231
+ animate pop 1.4
1232
+
1233
+ <span class="kw">add</span> section <span class="str">"#work"</span>:
1234
+ style padding <span class="str">"100px 24px"</span>
1235
+ style maxWidth <span class="str">"1100px"</span>
1236
+ style margin <span class="str">"0 auto"</span>
1237
+ <span class="kw">add</span> h2 <span class="str">""</span>:
1238
+ text <span class="str">"Selected Work"</span>
1239
+ style fontSize <span class="str">"2.5rem"</span>
1240
+ style fontWeight <span class="str">"800"</span>
1241
+ style marginBottom <span class="str">"48px"</span>
1242
+ animate slideIn 0.6
1243
+ <span class="kw">add</span> div <span class="str">".project-grid"</span>:
1244
+ style display <span class="str">"grid"</span>
1245
+ grid <span class="str">"repeat(auto-fill, minmax(320px,1fr))"</span>
1246
+ gap <span class="str">"24px"</span>
1247
+ media <span class="str">"max-width:640px"</span> gridTemplateColumns <span class="str">"1fr"</span>
1248
+
1249
+ <span class="kw">add</span> article <span class="str">".project-card"</span>:
1250
+ style background <span class="str">"#162b28"</span>
1251
+ style borderRadius <span class="str">"16px"</span>
1252
+ style overflow <span class="str">"hidden"</span>
1253
+ style border <span class="str">"1px solid #234440"</span>
1254
+ hover borderColor <span class="str">"#b8f000"</span>
1255
+ hover transform <span class="str">"translateY(-6px)"</span>
1256
+ hover boxShadow <span class="str">"0 16px 48px rgba(0,0,0,0.4)"</span>
1257
+ transition <span class="str">"all 0.25s ease"</span>
1258
+ animate slideUp 0.5
1259
+ <span class="kw">add</span> div <span class="str">""</span>:
1260
+ style padding <span class="str">"28px"</span>
1261
+ <span class="kw">add</span> h3 <span class="str">""</span>:
1262
+ text <span class="str">"Project Alpha"</span>
1263
+ style color <span class="str">"#b8f000"</span>
1264
+ style fontSize <span class="str">"1.2rem"</span>
1265
+ style fontWeight <span class="str">"700"</span>
1266
+ style marginBottom <span class="str">"8px"</span>
1267
+ <span class="kw">add</span> p <span class="str">""</span>:
1268
+ text <span class="str">"A high-performance web application built with modern tooling."</span>
1269
+ style color <span class="str">"#8ab8b4"</span>
1270
+ style lineHeight <span class="str">"1.6"</span>
1271
+ <span class="kw">add</span> a <span class="str">""</span>:
1272
+ text <span class="str">"View Project →"</span>
1273
+ href <span class="str">"#"</span>
1274
+ style display <span class="str">"inline-block"</span>
1275
+ style marginTop <span class="str">"16px"</span>
1276
+ style color <span class="str">"#b8f000"</span>
1277
+ style textDecoration <span class="str">"none"</span>
1278
+ style fontWeight <span class="str">"600"</span>
1279
+ style fontSize <span class="str">"0.9rem"</span>
1280
+ hover textDecoration <span class="str">"underline"</span>
1281
+
1282
+ <span class="kw">css</span>:
1283
+ ::selection { background: #b8f000; color: #0a1614 }
1284
+ ::-webkit-scrollbar { width: 5px }
1285
+ ::-webkit-scrollbar-thumb { background: #234440; border-radius: 3px }
1286
+ html { scroll-behavior: smooth }
1287
+ .project-card:nth-child(2) { animation-delay: 0.15s }
1288
+ .project-card:nth-child(3) { animation-delay: 0.3s }</div>
1289
+
1290
+ </div><!-- docs-section web mode -->
1291
+
1292
+ </div><!-- docs-page -->
1293
+ </div><!-- page-docs -->
855
1294
 
856
- </div>
857
- </div>
858
1295
 
859
1296
  <div id="input-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:999;align-items:center;justify-content:center;backdrop-filter:blur(3px)">
860
1297
  <div style="background:var(--surface);border:1px solid var(--border2);border-radius:12px;padding:24px 28px;min-width:360px;max-width:480px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.5)">
@@ -878,23 +1315,7 @@ code { font-family: 'DM Mono', monospace; font-size: 11px; color: var(--accent);
878
1315
 
879
1316
  </div><!-- app-body -->
880
1317
 
881
- <!-- INPUT MODAL -->
882
- <div id="input-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:999;align-items:center;justify-content:center;backdrop-filter:blur(3px)">
883
- <div style="background:var(--surface);border:1px solid var(--border2);border-radius:12px;padding:24px 28px;min-width:360px;max-width:480px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.5)">
884
- <div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
885
- <div style="width:28px;height:28px;border-radius:6px;background:var(--accent-dim);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px">⌨</div>
886
- <div>
887
- <div style="font-size:13px;font-weight:700;color:var(--ink)">Program needs input</div>
888
- <div style="font-size:10px;color:var(--muted);font-family:'DM Mono',monospace">Fill in values then click Run</div>
889
- </div>
890
- </div>
891
- <div id="input-modal-fields"></div>
892
- <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
893
- <button onclick="cancelInputModal()" style="padding:7px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border2);background:var(--surface2);color:var(--ink2);font-family:'Manrope',sans-serif">Cancel</button>
894
- <button onclick="submitInputModal()" style="padding:7px 20px;border-radius:6px;font-size:12px;font-weight:800;cursor:pointer;border:none;background:var(--lime);color:#0d1f1e;font-family:'Manrope',sans-serif">▶ Run</button>
895
- </div>
896
- </div>
897
- </div>
1318
+
898
1319
 
899
1320
  <!-- BROWSE DIALOG -->
900
1321
  <div class="browse-overlay" id="browse-overlay">
@@ -1977,94 +2398,160 @@ count i from 0 to 14:
1977
2398
  push(fibs, fibIterative(i))
1978
2399
  say fibs`,
1979
2400
 
1980
- webpage:`// StructScript Web — build a real webpage!
1981
-
1982
- page "My Page":
1983
- style background "#0d1f1e"
1984
- style fontFamily "Manrope, sans-serif"
1985
- style padding "40px"
1986
-
1987
- add div "hero":
1988
- style textAlign "center"
1989
- style padding "60px 20px"
1990
-
1991
- add h1 "title":
1992
- text "Hello from StructScript!"
1993
- style color "#b8f000"
1994
- style fontSize "48px"
1995
- style fontWeight "800"
1996
- style marginBottom "16px"
1997
- animate fadeIn 0.6
1998
-
1999
- add p "subtitle":
2000
- text "Building web pages with structured scripting"
2001
- style color "#8ab8b4"
2002
- style fontSize "18px"
2003
- style marginBottom "32px"
2004
- animate fadeIn 0.9
2005
-
2006
- add button "cta":
2007
- text "Click me!"
2008
- style background "#0b7a75"
2009
- style color "#b8f000"
2010
- style border "none"
2011
- style padding "14px 32px"
2012
- style borderRadius "8px"
2013
- style fontSize "16px"
2014
- style fontWeight "700"
2015
- style cursor "pointer"
2016
- animate pop 0.5
2017
- on click:
2018
- say "You clicked the button!"
2019
-
2020
- add div "cards":
2021
- style display "flex"
2022
- style gap "20px"
2023
- style justifyContent "center"
2024
- style marginTop "40px"
2025
- style flexWrap "wrap"
2026
-
2027
- add div "":
2028
- style background "#162b28"
2029
- style border "1px solid #234440"
2030
- style borderRadius "12px"
2031
- style padding "24px"
2032
- style width "180px"
2033
- style textAlign "center"
2034
- animate slideIn 0.4
2035
- add h3 "": text "Rocket Fast"
2036
- style color "#b8f000"
2037
- add p "": text "Runs in the browser"
2038
- style color "#8ab8b4"
2039
- style fontSize "13px"
2040
-
2041
- add div "":
2042
- style background "#162b28"
2043
- style border "1px solid #234440"
2044
- style borderRadius "12px"
2045
- style padding "24px"
2046
- style width "180px"
2047
- style textAlign "center"
2048
- animate slideIn 0.65
2049
- add h3 "": text "Super Simple"
2050
- style color "#b8f000"
2051
- add p "": text "English-like syntax"
2052
- style color "#8ab8b4"
2053
- style fontSize "13px"
2054
-
2055
- add div "":
2056
- style background "#162b28"
2057
- style border "1px solid #234440"
2058
- style borderRadius "12px"
2059
- style padding "24px"
2060
- style width "180px"
2061
- style textAlign "center"
2062
- animate slideIn 0.9
2063
- add h3 "": text "Fully Styled"
2064
- style color "#b8f000"
2065
- add p "": text "CSS control built in"
2066
- style color "#8ab8b4"
2067
- style fontSize "13px"`,
2401
+ webpage:`// StructScript Web Mode every HTML tag, CSS property & browser event!
2402
+ // Run this to see a full landing page with nav, hero, cards, form & animations.
2403
+
2404
+ page "My App":
2405
+ style background "#0a1614"
2406
+ style color "#e0f0ee"
2407
+ style fontFamily "system-ui, sans-serif"
2408
+ meta description "Built with StructScript"
2409
+
2410
+ add nav "":
2411
+ style display "flex"
2412
+ style justifyContent "space-between"
2413
+ style alignItems "center"
2414
+ style padding "0 32px"
2415
+ style height "56px"
2416
+ style background "rgba(10,22,20,0.95)"
2417
+ style borderBottom "1px solid #234440"
2418
+ style position "sticky"
2419
+ style top "0"
2420
+ style zIndex "100"
2421
+ add span "": text "MyApp" style fontWeight "800" style color "#b8f000"
2422
+ add div "":
2423
+ style display "flex" gap "20px"
2424
+ add a "": text "Home" href "#" style color "#8ab8b4" hover color "#b8f000" transition "color 0.2s" style textDecoration "none"
2425
+ add a "": text "About" href "#about" style color "#8ab8b4" hover color "#b8f000" transition "color 0.2s" style textDecoration "none"
2426
+ add a "": text "Contact" href "#contact" style color "#8ab8b4" hover color "#b8f000" transition "color 0.2s" style textDecoration "none"
2427
+
2428
+ add section "#hero":
2429
+ style minHeight "80vh"
2430
+ style display "flex"
2431
+ style flexDirection "column"
2432
+ style justifyContent "center"
2433
+ style alignItems "center"
2434
+ style textAlign "center"
2435
+ style padding "60px 24px"
2436
+ add h1 "":
2437
+ text "Build pages in StructScript"
2438
+ style fontSize "clamp(2rem,6vw,4rem)"
2439
+ style fontWeight "800"
2440
+ style letterSpacing "-1px"
2441
+ animate fadeIn 0.8
2442
+ add p "":
2443
+ text "Every HTML element. Every CSS property. Zero boilerplate."
2444
+ style color "#8ab8b4" style fontSize "1.1rem" style marginTop "16px"
2445
+ animate fadeIn 1.1
2446
+ add div "":
2447
+ style display "flex" gap "12px" style marginTop "36px" style flexWrap "wrap" style justifyContent "center"
2448
+ add button ".btn-primary":
2449
+ text "Get started"
2450
+ hover background "#d4ff33" hover transform "translateY(-2px)"
2451
+ active transform "scale(0.97)"
2452
+ transition "all 0.2s"
2453
+ animate pop 1.3
2454
+ on click:
2455
+ document.getElementById('about').scrollIntoView({behavior:'smooth'})
2456
+ add button ".btn-secondary":
2457
+ text "Learn more"
2458
+ hover borderColor "#b8f000" hover color "#b8f000"
2459
+ transition "all 0.2s"
2460
+ animate pop 1.5
2461
+
2462
+ add section "#about":
2463
+ style padding "80px 24px"
2464
+ style maxWidth "900px"
2465
+ style margin "0 auto"
2466
+ add h2 "": text "Features" style fontSize "2rem" style fontWeight "800" style marginBottom "40px" animate slideIn 0.5
2467
+ add div ".feature-grid":
2468
+ style display "grid"
2469
+ grid "repeat(auto-fill, minmax(240px, 1fr))"
2470
+ gap "20px"
2471
+ media "max-width:600px" gridTemplateColumns "1fr"
2472
+ add div ".feature-card":
2473
+ hover borderColor "#b8f000" hover transform "translateY(-4px)"
2474
+ transition "all 0.2s" animate slideUp 0.4
2475
+ add span "": text "🌐" style fontSize "2rem"
2476
+ add h3 "": text "Web-Ready" style color "#b8f000" style margin "12px 0 6px"
2477
+ add p "": text "Full HTML output. Share as a real webpage." style color "#8ab8b4" style fontSize "0.9rem"
2478
+ add div ".feature-card":
2479
+ hover borderColor "#b8f000" hover transform "translateY(-4px)"
2480
+ transition "all 0.2s" animate slideUp 0.55
2481
+ add span "": text "🎨" style fontSize "2rem"
2482
+ add h3 "": text "CSS Power" style color "#b8f000" style margin "12px 0 6px"
2483
+ add p "": text "Every property, pseudo-class, and animation." style color "#8ab8b4" style fontSize "0.9rem"
2484
+ add div ".feature-card":
2485
+ hover borderColor "#b8f000" hover transform "translateY(-4px)"
2486
+ transition "all 0.2s" animate slideUp 0.7
2487
+ add span "": text "⚡" style fontSize "2rem"
2488
+ add h3 "": text "Events" style color "#b8f000" style margin "12px 0 6px"
2489
+ add p "": text "Click, drag, keyboard, touch — all covered." style color "#8ab8b4" style fontSize "0.9rem"
2490
+
2491
+ add section "#contact":
2492
+ style padding "80px 24px" style background "#0d1f1e"
2493
+ add div "":
2494
+ style maxWidth "480px" style margin "0 auto"
2495
+ add h2 "": text "Get in touch" style fontSize "2rem" style fontWeight "800" style marginBottom "32px" animate slideIn 0.4
2496
+ add form "":
2497
+ style display "flex" style flexDirection "column" gap "14px"
2498
+ on submit:
2499
+ event.preventDefault();
2500
+ document.getElementById('form-msg').textContent = '✓ Sent!';
2501
+ this.reset();
2502
+ add input "":
2503
+ type "text" placeholder "Your name" required
2504
+ style padding "12px 16px" style borderRadius "8px"
2505
+ style border "1px solid #234440" style background "#162b28"
2506
+ style color "#e0f0ee"
2507
+ focus outline "none" focus borderColor "#b8f000"
2508
+ transition "border-color 0.2s"
2509
+ add input "":
2510
+ type "email" placeholder "email@example.com" required
2511
+ style padding "12px 16px" style borderRadius "8px"
2512
+ style border "1px solid #234440" style background "#162b28"
2513
+ style color "#e0f0ee"
2514
+ focus outline "none" focus borderColor "#b8f000"
2515
+ valid borderColor "#b8f000"
2516
+ invalid borderColor "#f07070"
2517
+ transition "border-color 0.2s"
2518
+ add textarea "":
2519
+ placeholder "Your message..." rows "4" required
2520
+ style padding "12px 16px" style borderRadius "8px"
2521
+ style border "1px solid #234440" style background "#162b28"
2522
+ style color "#e0f0ee" style resize "vertical"
2523
+ focus outline "none" focus borderColor "#b8f000"
2524
+ transition "border-color 0.2s"
2525
+ add button "":
2526
+ text "Send message" type "submit"
2527
+ style background "#0b7a75" style color "#b8f000"
2528
+ style border "none" style padding "12px 24px"
2529
+ style borderRadius "8px" style fontWeight "700"
2530
+ style cursor "pointer"
2531
+ hover background "#12a89e"
2532
+ transition "background 0.2s"
2533
+ add p "form-msg": style color "#b8f000" style fontWeight "600" style marginTop "8px"
2534
+
2535
+ css:
2536
+ ::selection { background: #b8f000; color: #0a1614 }
2537
+ ::-webkit-scrollbar { width: 5px }
2538
+ ::-webkit-scrollbar-thumb { background: #234440; border-radius: 3px }
2539
+ html { scroll-behavior: smooth }
2540
+ .btn-primary {
2541
+ background: #b8f000; color: #0a1614;
2542
+ border: none; padding: 13px 32px;
2543
+ border-radius: 50px; font-weight: 700;
2544
+ font-size: 1rem; cursor: pointer;
2545
+ }
2546
+ .btn-secondary {
2547
+ background: transparent; color: #e0f0ee;
2548
+ border: 1.5px solid #234440; padding: 13px 32px;
2549
+ border-radius: 50px; font-weight: 600;
2550
+ font-size: 1rem; cursor: pointer;
2551
+ }
2552
+ .feature-card { background: #162b28; border: 1px solid #234440; border-radius: 14px; padding: 28px; }
2553
+ .feature-grid .feature-card:nth-child(2) { animation-delay: 0.15s }
2554
+ .feature-grid .feature-card:nth-child(3) { animation-delay: 0.3s }`,
2068
2555
 
2069
2556
  fizzbuzz:`// FizzBuzz — classic interview question
2070
2557
 
@@ -2314,6 +2801,8 @@ function updateAll() {
2314
2801
  updateLineNumbers();
2315
2802
  markModified(true);
2316
2803
  }
2804
+ function updateHighlight() { highlightLayer.innerHTML = highlight(editor.value); highlightLayer.scrollTop = editor.scrollTop; highlightLayer.scrollLeft = editor.scrollLeft; }
2805
+ function setModified(v) { markModified(v); }
2317
2806
 
2318
2807
  function markModified(v) {
2319
2808
  isModified = v;
@@ -2487,250 +2976,332 @@ function _execCode(code, inputValues) {
2487
2976
  // ============================================================
2488
2977
 
2489
2978
  // Detects if the code uses any web commands
2490
- function isWebCode(code) {
2491
- return /^\s*(page|add)\s+/m.test(code);
2492
- }
2493
-
2494
- // Build a full HTML document from StructScript web commands
2495
- function buildWebDoc(code) {
2496
- let pageTitle = 'StructScript Page';
2497
- let pageStyles = {};
2498
- let cssBlocks = []; // raw CSS rule strings from `css` blocks
2499
- const elements = [];
2500
- const elStack = [];
2501
- let i = 0;
2502
-
2503
- const lines = code.split('\n');
2504
-
2505
- function getIndent(l) { let n=0; while(n<l.length&&l[n]===' ')n++; return n; }
2506
-
2507
- function parseStr(s) {
2508
- s = s.trim();
2509
- if((s.startsWith('"')&&s.endsWith('"'))||(s.startsWith("'")&&s.endsWith("'"))) return s.slice(1,-1);
2510
- return s;
2511
- }
2512
-
2513
- // camelCase kebab-case
2514
- function toKebab(s) { return s.replace(/([A-Z])/g,'-$1').toLowerCase(); }
2515
- // kebab-case or camelCase → camelCase (for object keys)
2516
- function toCamel(s) { return s.replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); }
2517
-
2518
- while (i < lines.length) {
2519
- const raw = lines[i], t = raw.trim(), ind = getIndent(raw);
2520
- i++;
2521
- if (!t || t.startsWith('//')) continue;
2522
-
2523
- // ── page "title": ──────────────────────────────────────
2524
- if (/^page\b/.test(t)) {
2525
- const m = t.match(/^page\s+"([^"]*)"|^page\s+\'([^']*)\'/);
2526
- if (m) pageTitle = m[1]||m[2];
2527
- const el = { _type:'page', styles:{}, attrs:{} };
2528
- elStack.length = 0;
2529
- elStack.push({ el, indent: ind });
2530
- continue;
2531
- }
2532
-
2533
- // ── css: (raw CSS block) ────────────────────────────────
2534
- // css:
2535
- // .btn { background: red }
2536
- // @media (max-width: 600px) { ... }
2537
- if (/^css\s*:?$/.test(t)) {
2538
- const cssLines = [];
2539
- while (i < lines.length) {
2540
- const nr = lines[i], ni = getIndent(nr);
2541
- if (nr.trim() === '' || ni > ind) { cssLines.push(nr.slice(ind+2||0)); i++; }
2542
- else break;
2543
- }
2544
- cssBlocks.push(cssLines.join('\n'));
2545
- continue;
2546
- }
2547
-
2548
- // ── add TAG "id": ───────────────────────────────────────
2549
- const addM = t.match(/^add\s+(\w+)(?:\s+"([^"]*)")?(?:\s+\'([^']*)\')?\s*:?$/);
2550
- if (addM) {
2551
- while (elStack.length > 1 && elStack[elStack.length-1].indent >= ind) elStack.pop();
2552
- const parent = elStack[elStack.length-1]?.el || null;
2553
- const rawId = addM[2]||addM[3]||'';
2554
- const el = {
2555
- tag: addM[1].toLowerCase(),
2556
- id: rawId.startsWith('#') ? rawId.slice(1) : (rawId.includes(' ')||rawId.startsWith('.')?'':rawId),
2557
- classes: rawId.startsWith('.') ? [rawId.slice(1)] : (rawId.includes(' ') ? rawId.split(' ') : []),
2558
- text:'', html:'', styles:{}, hoverStyles:{}, focusStyles:{}, attrs:{}, events:[], children:[],
2559
- _indent: ind, _anim: null, _cssRules: [],
2560
- };
2561
- if (parent && parent._type !== 'page') parent.children.push(el);
2562
- else elements.push(el);
2563
- elStack.push({ el, indent: ind });
2564
- continue;
2565
- }
2566
-
2567
- // ── Properties inside an element ───────────────────────
2568
- const parentEntry = elStack[elStack.length-1];
2569
- if (parentEntry && ind > parentEntry.indent) {
2570
- const el = parentEntry.el;
2571
-
2572
- // text / html
2573
- const textM = t.match(/^text\s+(.+)$/);
2574
- if (textM) { el.text = parseStr(textM[1]); continue; }
2575
- const htmlM = t.match(/^html\s+(.+)$/);
2576
- if (htmlM) { el.html = parseStr(htmlM[1]); continue; }
2577
-
2578
- // style prop "value" — any valid CSS property (camel or kebab)
2579
- const styleM = t.match(/^style\s+([\w-]+)\s*:?\s+(.+)$/);
2580
- if (styleM) {
2581
- const key = toCamel(styleM[1]);
2582
- const val = parseStr(styleM[2]);
2583
- el.styles[key] = val;
2584
- if (el._type === 'page') pageStyles[key] = val;
2585
- continue;
2586
- }
2587
-
2588
- // hover prop "value" — generates :hover CSS rule
2589
- const hoverM = t.match(/^hover\s+([\w-]+)\s*:?\s+(.+)$/);
2590
- if (hoverM) {
2591
- el.hoverStyles[toCamel(hoverM[1])] = parseStr(hoverM[2]);
2592
- continue;
2593
- }
2594
-
2595
- // focus prop "value" — generates :focus CSS rule
2596
- const focusM = t.match(/^focus\s+([\w-]+)\s*:?\s+(.+)$/);
2597
- if (focusM) {
2598
- el.focusStyles[toCamel(focusM[1])] = parseStr(focusM[2]);
2599
- continue;
2600
- }
2601
-
2602
- // transition "prop duration easing"
2603
- const transM = t.match(/^transition\s+(.+)$/);
2604
- if (transM) { el.styles.transition = parseStr(transM[1]); continue; }
2605
-
2606
- // attr / class
2607
- const attrM = t.match(/^attr\s+(\w+)\s+(.+)$/);
2608
- if (attrM) { el.attrs[attrM[1]] = parseStr(attrM[2]); continue; }
2609
- const classM = t.match(/^class\s+(.+)$/);
2610
- if (classM) { el.classes.push(parseStr(classM[1])); continue; }
2611
-
2612
- // animate TYPE DURATION EASING
2613
- const animM = t.match(/^animate\s+(\w+)(?:\s+([\d.]+))?(?:\s+(\w+))?$/);
2614
- if (animM) {
2615
- el._anim = { type: animM[1], dur: parseFloat(animM[2]||0.4), easing: animM[3]||'ease' };
2616
- continue;
2617
- }
2618
-
2619
- // on EVENT: (collect indented handler lines)
2620
- const onM = t.match(/^on\s+(\w+)\s*:?$/);
2621
- if (onM) {
2622
- const handlerLines = [];
2623
- while (i < lines.length) {
2624
- const nr = lines[i], ni = getIndent(nr);
2625
- if (!nr.trim() || ni <= ind) break;
2626
- handlerLines.push(nr.slice(ni)); i++;
2627
- }
2628
- el.events.push({ event: onM[1], code: handlerLines.join('\n') });
2629
- continue;
2630
- }
2631
- }
2632
- }
2633
-
2634
- // ── Render ──────────────────────────────────────────────────
2635
- const pseudoRules = []; // accumulated :hover, :focus rules
2636
-
2637
- function renderEl(el) {
2638
- if (el._type === 'page') return '';
2639
- const tag = el.tag;
2640
- const idAttr = el.id ? ` id="${el.id}"` : '';
2641
- const clsAttr = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
2642
-
2643
- // Inline styles
2644
- let styleStr = Object.entries(el.styles).map(([k,v]) => `${toKebab(k)}:${v}`).join(';');
2645
-
2646
- // Animation
2647
- if (el._anim) {
2648
- const animMap = {
2649
- fadeIn: `fadeIn ${el._anim.dur}s ${el._anim.easing} forwards`,
2650
- fadeOut: `fadeOut ${el._anim.dur}s ${el._anim.easing} forwards`,
2651
- slideIn: `slideIn ${el._anim.dur}s ${el._anim.easing} forwards`,
2652
- slideUp: `slideUp ${el._anim.dur}s ${el._anim.easing} forwards`,
2653
- bounce: `bounce ${el._anim.dur}s ${el._anim.easing} infinite`,
2654
- pulse: `pulse ${el._anim.dur}s ${el._anim.easing} infinite`,
2655
- spin: `spin ${el._anim.dur}s linear infinite`,
2656
- shake: `shake ${el._anim.dur}s ${el._anim.easing}`,
2657
- pop: `pop ${el._anim.dur}s ${el._anim.easing} forwards`,
2658
- };
2659
- styleStr += (styleStr?';':'') + `animation:${animMap[el._anim.type]||`${el._anim.type} ${el._anim.dur}s ${el._anim.easing}`}`;
2660
- }
2661
-
2662
- // Generate :hover and :focus rules (needs a selector — use id if available, else generate one)
2663
- if (Object.keys(el.hoverStyles).length || Object.keys(el.focusStyles).length) {
2664
- // Ensure element has an id for targeting
2665
- if (!el.id) { el.id = '_ss_' + Math.random().toString(36).slice(2,8); }
2666
- if (Object.keys(el.hoverStyles).length) {
2667
- const hoverStr = Object.entries(el.hoverStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
2668
- pseudoRules.push(`#${el.id}:hover{${hoverStr}}`);
2669
- }
2670
- if (Object.keys(el.focusStyles).length) {
2671
- const focusStr = Object.entries(el.focusStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
2672
- pseudoRules.push(`#${el.id}:focus{${focusStr}}`);
2673
- }
2674
- }
2675
-
2676
- const styleAttr = styleStr ? ` style="${styleStr}"` : '';
2677
- const extraAttrs = Object.entries(el.attrs).map(([k,v])=>` ${k}="${v}"`).join('');
2678
- const evAttrs = el.events.map(ev=>{
2679
- const escaped = ev.code.replace(/"/g,'&quot;').replace(/\n/g,' ');
2680
- return ` data-ss-on${ev.event}="${escaped}"`;
2681
- }).join('');
2682
-
2683
- const selfClose = ['img','input','br','hr','meta','link'].includes(tag);
2684
- if (selfClose) return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}/>
2685
- `;
2686
- const inner = el.html || el.text || el.children.map(renderEl).join('');
2687
- return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}${evAttrs}>${inner}</${tag}>
2688
- `;
2689
- }
2690
-
2691
- const bodyStyleStr = Object.entries(pageStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
2692
- const bodyHtml = elements.map(renderEl).join('');
2693
-
2694
- const KEYFRAMES = `
2695
- @keyframes fadeIn { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:none} }
2696
- @keyframes fadeOut { from{opacity:1} to{opacity:0} }
2697
- @keyframes slideIn { from{transform:translateX(-40px);opacity:0} to{transform:none;opacity:1} }
2698
- @keyframes slideUp { from{transform:translateY(40px);opacity:0} to{transform:none;opacity:1} }
2699
- @keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-16px)} }
2700
- @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
2701
- @keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
2702
- @keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-8px)} 75%{transform:translateX(8px)} }
2703
- @keyframes pop { 0%{transform:scale(0.5);opacity:0} 70%{transform:scale(1.1)} 100%{transform:scale(1);opacity:1} }
2704
- `;
2705
-
2706
- const EVENT_TYPES = ['click','mouseover','mouseout','mouseenter','mouseleave','keydown','keyup','change','focus','blur','dblclick'];
2707
- const evScript = EVENT_TYPES.map(ev =>
2708
- `document.querySelectorAll('[data-ss-on${ev}]').forEach(function(el){el.addEventListener('${ev}',function(){try{(new Function(this.getAttribute('data-ss-on${ev}')))()}catch(e){console.error(e)}}.bind(el));});`
2709
- ).join('\n');
2710
-
2711
- const allPseudoCSS = pseudoRules.join('\n');
2712
- const allCustomCSS = cssBlocks.join('\n');
2713
-
2714
- return `<!DOCTYPE html>
2715
- <html lang="en">
2716
- <head>
2717
- <meta charset="UTF-8">
2718
- <meta name="viewport" content="width=device-width,initial-scale=1">
2719
- <title>${pageTitle}</title>
2720
- <style>
2721
- *{box-sizing:border-box;margin:0;padding:0}
2722
- ${KEYFRAMES}
2723
- ${allPseudoCSS}
2724
- ${allCustomCSS}
2725
- </style>
2726
- </head>
2727
- <body${bodyStyleStr ? ` style="${bodyStyleStr}"` : ''}>
2728
- ${bodyHtml}
2729
- <script>(function(){${evScript}})()</\/script>
2730
- </body>
2731
- </html>`;
2732
- }
2733
-
2979
+
2980
+ function isWebCode(code) {
2981
+ return /^\s*(page\b|add\s+\w|css\s*:?$|script\s*:?$)/m.test(code);
2982
+ }
2983
+
2984
+ function buildWebDoc(rawCode) {
2985
+ // Normalise line endings
2986
+ const code = rawCode.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
2987
+
2988
+ let pageTitle = 'StructScript Page';
2989
+ let pageLang = 'en';
2990
+ let pageStyles = {};
2991
+ let headTags = [];
2992
+ let cssBlocks = [];
2993
+ let jsBlocks = [];
2994
+ let bodyAttrs = {};
2995
+ const elements = [];
2996
+ const elStack = [];
2997
+ let i = 0;
2998
+ let _uid = 0;
2999
+ const lines = code.split('\n');
3000
+
3001
+ function indent(l) { let n=0; while(n<l.length&&l[n]===' ')n++; return n; }
3002
+ function str(s) { s=s.trim(); return (s[0]==='"'&&s[s.length-1]==='"')||(s[0]==="'"&&s[s.length-1]==="'") ? s.slice(1,-1) : s; }
3003
+ function kebab(s) { return s.replace(/([A-Z])/g,'-$1').toLowerCase(); }
3004
+ function camel(s) { return s.replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); }
3005
+ function uid() { return '_ss'+(++_uid); }
3006
+
3007
+ // ── parse one line, advancing i if it consumes more ──────────
3008
+ while (i < lines.length) {
3009
+ const raw = lines[i], t = raw.trim(), ind = indent(raw);
3010
+ i++;
3011
+ if (!t || t.startsWith('//')) continue;
3012
+
3013
+ // page "Title" lang? :
3014
+ if (/^page\b/.test(t)) {
3015
+ const m = t.match(/^page\s+"([^"]*)"|^page\s+'([^']*)'/);
3016
+ if (m) pageTitle = m[1]||m[2];
3017
+ const lm = t.match(/\blang\s+"([^"]+)"/);
3018
+ if (lm) pageLang = lm[1];
3019
+ // consume indented page-level directives
3020
+ while (i < lines.length) {
3021
+ const nr = lines[i].trim(), ni = indent(lines[i]);
3022
+ if (!nr || nr.startsWith('//')) { i++; continue; }
3023
+ if (ni <= ind) break;
3024
+ i++;
3025
+ let m2;
3026
+ if ((m2=nr.match(/^style\s+([\w-]+)\s+(.+)$/))) { pageStyles[camel(m2[1])]=str(m2[2]); continue; }
3027
+ if ((m2=nr.match(/^charset\s+(.+)$/))) { headTags.push(`<meta charset="${str(m2[1])}">`); continue; }
3028
+ if ((m2=nr.match(/^viewport\s+(.+)$/))) { headTags.push(`<meta name="viewport" content="${str(m2[1])}">`); continue; }
3029
+ if ((m2=nr.match(/^meta\s+([\w-]+)\s+(.+)$/))) { headTags.push(`<meta name="${m2[1]}" content="${str(m2[2])}">`); continue; }
3030
+ if ((m2=nr.match(/^metahttp\s+([\w-]+)\s+(.+)$/))) { headTags.push(`<meta http-equiv="${m2[1]}" content="${str(m2[2])}">`); continue; }
3031
+ if ((m2=nr.match(/^link\s+(.+)$/))) { headTags.push(`<link ${str(m2[1])}>`); continue; }
3032
+ if ((m2=nr.match(/^favicon\s+(.+)$/))) { headTags.push(`<link rel="icon" href="${str(m2[1])}">`); continue; }
3033
+ if ((m2=nr.match(/^font\s+(.+)$/))) { headTags.push(`<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${str(m2[1]).replace(/ /g,'+')}:wght@300;400;600;700;800&display=swap">`); continue; }
3034
+ if ((m2=nr.match(/^script\s+src\s+(.+)$/))) { headTags.push(`<script src="${str(m2[1])}"><\/script>`); continue; }
3035
+ if ((m2=nr.match(/^bodyattr\s+([\w-]+)\s+(.+)$/))) { bodyAttrs[m2[1]]=str(m2[2]); continue; }
3036
+ if ((m2=nr.match(/^(og|twitter):([\w:]+)\s+(.+)$/))){ headTags.push(`<meta property="${m2[1]}:${m2[2]}" content="${str(m2[3])}">`); continue; }
3037
+ if ((m2=nr.match(/^canonical\s+(.+)$/))) { headTags.push(`<link rel="canonical" href="${str(m2[1])}">`); continue; }
3038
+ if ((m2=nr.match(/^base\s+(.+)$/))) { headTags.push(`<base href="${str(m2[1])}">`); continue; }
3039
+ }
3040
+ continue;
3041
+ }
3042
+
3043
+ // css: raw CSS block
3044
+ if (/^css\s*:?$/.test(t)) {
3045
+ const buf=[];
3046
+ while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni>ind){buf.push(nr.slice(Math.min(ni,ind+2)));i++;}else break; }
3047
+ cssBlocks.push(buf.join('\n'));
3048
+ continue;
3049
+ }
3050
+
3051
+ // script: raw JS block
3052
+ if (/^script\s*:?$/.test(t)) {
3053
+ const buf=[];
3054
+ while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni>ind){buf.push(nr.slice(Math.min(ni,ind+2)));i++;}else break; }
3055
+ jsBlocks.push(buf.join('\n'));
3056
+ continue;
3057
+ }
3058
+
3059
+ // add TAG "id/classes": — the workhorse
3060
+ // Supports: add div "myId" add div ".btn.active" add div "#main .hero big"
3061
+ const addM = t.match(/^add\s+([\w-]+)(?:\s+"([^"]*)")?(?:\s+'([^']*)')?\s*:?$/);
3062
+ if (addM) {
3063
+ while (elStack.length>1 && elStack[elStack.length-1].indent>=ind) elStack.pop();
3064
+ const parent = elStack.length ? elStack[elStack.length-1].el : null;
3065
+ const rawSel = (addM[2]||addM[3]||'').trim();
3066
+ const el = {
3067
+ tag:addM[1].toLowerCase(), id:'', classes:[],
3068
+ text:'', html:'',
3069
+ styles:{}, pseudo:{}, mediaRules:[],
3070
+ beforeContent:null, afterContent:null,
3071
+ attrs:{}, events:[], children:[],
3072
+ _indent:ind, _anim:null,
3073
+ };
3074
+ // parse selector tokens: #id .class plain-word→id
3075
+ rawSel.split(/\s+/).filter(Boolean).forEach(tok => {
3076
+ if (tok.startsWith('#')) el.id = tok.slice(1);
3077
+ else if (tok.startsWith('.')) el.classes.push(tok.slice(1));
3078
+ else if (!el.id) el.id = tok;
3079
+ else el.classes.push(tok);
3080
+ });
3081
+ if (parent && !parent._isPage) parent.children.push(el);
3082
+ else elements.push(el);
3083
+ elStack.push({el, indent:ind});
3084
+ continue;
3085
+ }
3086
+
3087
+ // ── properties inside an element ─────────────────────────
3088
+ const pe = elStack[elStack.length-1];
3089
+ if (!pe || ind <= pe.indent) continue;
3090
+ const el = pe.el;
3091
+
3092
+ let m2;
3093
+
3094
+ // content
3095
+ if ((m2=t.match(/^text\s+(.+)$/))) { el.text=str(m2[1]); continue; }
3096
+ if ((m2=t.match(/^html\s+(.+)$/))) { el.html=str(m2[1]); continue; }
3097
+
3098
+ // common attrs — shorthand keywords
3099
+ const ATTR_KW = {
3100
+ src:1,href:1,alt:1,title:1,type:1,name:1,value:1,
3101
+ for:1,action:1,method:1,target:1,rel:1,rows:1,cols:1,
3102
+ min:1,max:1,step:1,role:1,tabindex:1,colspan:1,rowspan:1,
3103
+ width:1,height:1,loading:1,decoding:1,crossorigin:1,
3104
+ enctype:1,accept:1,pattern:1,maxlength:1,minlength:1,
3105
+ download:1,sizes:1,srcset:1,poster:1,preload:1,
3106
+ sandbox:1,allow:1,frameborder:1,scrolling:1,
3107
+ };
3108
+ if ((m2=t.match(/^([\w-]+)\s+(.+)$/)) && ATTR_KW[m2[1]]) { el.attrs[m2[1]]=str(m2[2]); continue; }
3109
+
3110
+ // boolean attrs
3111
+ if (/^(checked|disabled|readonly|required|autoplay|loop|controls|muted|multiple|autofocus|hidden|open|selected|novalidate|async|defer|reversed|ismap|allowfullscreen|default|formnovalidate|spellcheck)$/.test(t))
3112
+ { el.attrs[t]=''; continue; }
3113
+
3114
+ // aria-*, data-*
3115
+ if ((m2=t.match(/^aria-([\w-]+)\s+(.+)$/))) { el.attrs[`aria-${m2[1]}`]=str(m2[2]); continue; }
3116
+ if ((m2=t.match(/^data-([\w-]+)\s+(.+)$/))) { el.attrs[`data-${m2[1]}`]=str(m2[2]); continue; }
3117
+
3118
+ // generic attr fallback
3119
+ if ((m2=t.match(/^attr\s+([\w-]+)\s+(.+)$/))) { el.attrs[m2[1]]=str(m2[2]); continue; }
3120
+
3121
+ // class
3122
+ if ((m2=t.match(/^class\s+(.+)$/))) { str(m2[1]).split(/\s+/).forEach(c=>c&&el.classes.push(c)); continue; }
3123
+
3124
+ // style any CSS property, camelCase or kebab
3125
+ if ((m2=t.match(/^style\s+([\w-]+)\s+(.+)$/))) { el.styles[camel(m2[1])]=str(m2[2]); continue; }
3126
+
3127
+ // pseudo-class styles: hover / focus / active / visited / checked / disabled / focusVisible / focusWithin / placeholder / selection
3128
+ if ((m2=t.match(/^(hover|focus|active|visited|checked|disabled|focus-visible|focus-within|placeholder|first-child|last-child|nth-child|not|invalid|valid)\s+([\w-]+)\s+(.+)$/)))
3129
+ { if(!el.pseudo[m2[1]])el.pseudo[m2[1]]={}; el.pseudo[m2[1]][camel(m2[2])]=str(m2[3]); continue; }
3130
+
3131
+ // pseudo-elements: before / after / first-line / first-letter / marker / selection / backdrop
3132
+ if ((m2=t.match(/^before\s+(.+)$/))) { el.beforeContent=str(m2[1]); continue; }
3133
+ if ((m2=t.match(/^after\s+(.+)$/))) { el.afterContent=str(m2[1]); continue; }
3134
+
3135
+ // shorthand style helpers
3136
+ if ((m2=t.match(/^transition\s+(.+)$/))) { el.styles.transition=str(m2[1]); continue; }
3137
+ if ((m2=t.match(/^transform\s+(.+)$/))) { el.styles.transform=str(m2[1]); continue; }
3138
+ if ((m2=t.match(/^filter\s+(.+)$/))) { el.styles.filter=str(m2[1]); continue; }
3139
+ if ((m2=t.match(/^grid\s+(.+)$/))) { el.styles.gridTemplateColumns=str(m2[1]); continue; }
3140
+ if ((m2=t.match(/^flex\s+(.+)$/))) { el.styles.flex=str(m2[1]); continue; }
3141
+ if ((m2=t.match(/^gap\s+(.+)$/))) { el.styles.gap=str(m2[1]); continue; }
3142
+ if ((m2=t.match(/^bg\s+(.+)$/))) { el.styles.background=str(m2[1]); continue; }
3143
+ if ((m2=t.match(/^shadow\s+(.+)$/))) { el.styles.boxShadow=str(m2[1]); continue; }
3144
+ if ((m2=t.match(/^clip\s+(.+)$/))) { el.styles.clipPath=str(m2[1]); continue; }
3145
+ if ((m2=t.match(/^mask\s+(.+)$/))) { el.styles.mask=str(m2[1]); continue; }
3146
+ if ((m2=t.match(/^outline\s+(.+)$/))) { el.styles.outline=str(m2[1]); continue; }
3147
+ if ((m2=t.match(/^overflow\s+(.+)$/))) { el.styles.overflow=str(m2[1]); continue; }
3148
+ if ((m2=t.match(/^cursor\s+(.+)$/))) { el.styles.cursor=str(m2[1]); continue; }
3149
+ if ((m2=t.match(/^opacity\s+(.+)$/))) { el.styles.opacity=str(m2[1]); continue; }
3150
+ if ((m2=t.match(/^zindex\s+(.+)$/))) { el.styles.zIndex=str(m2[1]); continue; }
3151
+ if ((m2=t.match(/^var\s+(--[\w-]+)\s+(.+)$/))) { el.styles[m2[1]]=str(m2[2]); continue; }
3152
+
3153
+ // @media per-element: media "max-width:600px" prop value
3154
+ if ((m2=t.match(/^media\s+"([^"]+)"\s+([\w-]+)\s+(.+)$/)))
3155
+ { el.mediaRules.push({q:m2[1], p:camel(m2[2]), v:str(m2[3])}); continue; }
3156
+ // media "value" (HTML attribute, e.g. on <link> or <source>)
3157
+ if ((m2=t.match(/^media\s+(.+)$/)) && !m2[1].trim().match(/^"[^"]*"\s+[\w-]+/))
3158
+ { el.attrs.media=str(m2[1]); continue; }
3159
+ // placeholder "text" as HTML attribute (not pseudo-class)
3160
+ if ((m2=t.match(/^placeholder\s+(.+)$/)) && !m2[1].match(/^[\w-]/))
3161
+ { el.attrs.placeholder=str(m2[1]); continue; }
3162
+
3163
+ // animate NAME DURATION? EASING?
3164
+ if ((m2=t.match(/^animate\s+(\w+)(?:\s+([\d.]+))?(?:\s+([\w-]+))?$/)))
3165
+ { el._anim={name:m2[1], dur:parseFloat(m2[2]||0.4), ease:m2[3]||'ease'}; continue; }
3166
+
3167
+ // on EVENT: — raw JS handler, indented body
3168
+ if ((m2=t.match(/^on\s+([\w]+)\s*:?$/))) {
3169
+ const buf=[];
3170
+ while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni<=ind)break; buf.push(nr.slice(ni)); i++; }
3171
+ el.events.push({ev:m2[1], code:buf.join('\n')});
3172
+ continue;
3173
+ }
3174
+ }
3175
+
3176
+ // ── Render ────────────────────────────────────────────────
3177
+ const pseudoCSS = [];
3178
+ const mediaCSS = {};
3179
+
3180
+ const VOID = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
3181
+
3182
+ function ensureId(el) { if (!el.id) el.id=uid(); return el.id; }
3183
+
3184
+ function renderEl(el) {
3185
+ const tag = el.tag;
3186
+
3187
+ // Build inline style string
3188
+ let styleStr = Object.entries(el.styles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
3189
+
3190
+ // Animations
3191
+ if (el._anim) {
3192
+ const ANIMS = {
3193
+ fadeIn:'fadeIn',fadeOut:'fadeOut',
3194
+ slideIn:'slideIn',slideInRight:'slideInRight',
3195
+ slideUp:'slideUp',slideDown:'slideDown',
3196
+ pop:'pop',bounce:'bounce',spin:'spin',
3197
+ pulse:'pulse',shake:'shake',flip:'flip',
3198
+ zoom:'zoom',wiggle:'wiggle',typewriter:'typewriter',
3199
+ };
3200
+ const aname = ANIMS[el._anim.name]||el._anim.name;
3201
+ const fill = /^(bounce|spin|pulse)$/.test(el._anim.name)?'infinite':'both';
3202
+ const ease = el._anim.name==='spin'?'linear':el._anim.ease;
3203
+ styleStr += (styleStr?';':'')+`animation:${aname} ${el._anim.dur}s ${ease} ${fill}`;
3204
+ }
3205
+
3206
+ // Pseudo-class rules
3207
+ Object.entries(el.pseudo).forEach(([pseudo, styles]) => {
3208
+ const r = Object.entries(styles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
3209
+ if (r) { ensureId(el); pseudoCSS.push(`#${el.id}:${pseudo}{${r}}`); }
3210
+ });
3211
+
3212
+ // before/after pseudo-elements
3213
+ if (el.beforeContent!==null) { ensureId(el); pseudoCSS.push(`#${el.id}::before{content:"${el.beforeContent.replace(/"/g,'\\"')}"}`); }
3214
+ if (el.afterContent!==null) { ensureId(el); pseudoCSS.push(`#${el.id}::after{content:"${el.afterContent.replace(/"/g,'\\"')}"}`); }
3215
+
3216
+ // @media rules
3217
+ el.mediaRules.forEach(({q,p,v}) => {
3218
+ const s = el.id ? `#${el.id}` : (el.classes[0] ? `.${el.classes[0]}` : tag);
3219
+ if (!mediaCSS[q]) mediaCSS[q]=[];
3220
+ mediaCSS[q].push(`${s}{${kebab(p)}:${v}}`);
3221
+ });
3222
+
3223
+ // Build attribute string
3224
+ const idA = el.id ? ` id="${el.id}"` : '';
3225
+ const clsA = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
3226
+ const styA = styleStr ? ` style="${styleStr}"` : '';
3227
+ const xtra = Object.entries(el.attrs).map(([k,v])=>v===''?` ${k}`:` ${k}="${v}"`).join('');
3228
+ const evts = el.events.map(({ev,code})=>{
3229
+ return ` data-ss-${ev}="${code.replace(/"/g,'&quot;').replace(/\n/g,' ')}"`;
3230
+ }).join('');
3231
+
3232
+ if (VOID.has(tag)) return `<${tag}${idA}${clsA}${styA}${xtra}>\n`;
3233
+ const inner = el.html || el.text || el.children.map(renderEl).join('');
3234
+ return `<${tag}${idA}${clsA}${styA}${xtra}${evts}>${inner}</${tag}>\n`;
3235
+ }
3236
+
3237
+ const bodyStyleStr = Object.entries(pageStyles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
3238
+ const bodyXtra = Object.entries(bodyAttrs).map(([k,v])=>` ${k}="${v}"`).join('');
3239
+ const bodyHtml = elements.map(renderEl).join('');
3240
+
3241
+ const mediaCSSStr = Object.entries(mediaCSS)
3242
+ .map(([q,rules])=>`@media (${q}){${rules.join('')}}`).join('\n');
3243
+
3244
+ const KEYFRAMES = `
3245
+ @keyframes fadeIn {0%{opacity:0;transform:translateY(16px)}100%{opacity:1;transform:none}}
3246
+ @keyframes fadeOut {0%{opacity:1}100%{opacity:0}}
3247
+ @keyframes slideIn {0%{transform:translateX(-40px);opacity:0}100%{transform:none;opacity:1}}
3248
+ @keyframes slideInRight {0%{transform:translateX(40px);opacity:0}100%{transform:none;opacity:1}}
3249
+ @keyframes slideUp {0%{transform:translateY(40px);opacity:0}100%{transform:none;opacity:1}}
3250
+ @keyframes slideDown {0%{transform:translateY(-40px);opacity:0}100%{transform:none;opacity:1}}
3251
+ @keyframes bounce {0%,100%{transform:translateY(0)}50%{transform:translateY(-18px)}}
3252
+ @keyframes pulse {0%,100%{opacity:1}50%{opacity:0.4}}
3253
+ @keyframes spin {to{transform:rotate(360deg)}}
3254
+ @keyframes shake {0%,100%{transform:translateX(0)}20%{transform:translateX(-8px)}40%{transform:translateX(8px)}60%{transform:translateX(-5px)}80%{transform:translateX(5px)}}
3255
+ @keyframes pop {0%{transform:scale(0.5);opacity:0}70%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}
3256
+ @keyframes flip {0%{transform:rotateY(-90deg);opacity:0}100%{transform:rotateY(0);opacity:1}}
3257
+ @keyframes zoom {0%{transform:scale(0);opacity:0}100%{transform:scale(1);opacity:1}}
3258
+ @keyframes wiggle {0%,100%{transform:rotate(0)}25%{transform:rotate(-8deg)}75%{transform:rotate(8deg)}}
3259
+ @keyframes typewriter {from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}`;
3260
+
3261
+ // All DOM events wired up
3262
+ const ALL_EVENTS = [
3263
+ 'click','dblclick','contextmenu',
3264
+ 'mousedown','mouseup','mouseover','mouseout','mouseenter','mouseleave','mousemove',
3265
+ 'keydown','keyup','keypress',
3266
+ 'input','change','focus','blur','submit','reset','select',
3267
+ 'scroll','resize','wheel',
3268
+ 'dragstart','drag','dragend','dragover','dragenter','dragleave','drop',
3269
+ 'touchstart','touchmove','touchend','touchcancel',
3270
+ 'pointerdown','pointermove','pointerup','pointercancel','pointerenter','pointerleave',
3271
+ 'animationstart','animationend','animationiteration',
3272
+ 'transitionstart','transitionend',
3273
+ 'load','error','abort','canplay','play','pause','ended','timeupdate','volumechange',
3274
+ 'copy','cut','paste','beforeinput',
3275
+ 'fullscreenchange','visibilitychange',
3276
+ ];
3277
+ const evScript = ALL_EVENTS.map(ev =>
3278
+ `document.querySelectorAll('[data-ss-${ev}]').forEach(function(el){el.addEventListener('${ev}',function(event){try{(new Function('event',this.getAttribute('data-ss-${ev}'))).call(this,event)}catch(e){console.error(e)}}.bind(el));});`
3279
+ ).join('\n');
3280
+
3281
+ return `<!DOCTYPE html>
3282
+ <html lang="${pageLang}">
3283
+ <head>
3284
+ <meta charset="UTF-8">
3285
+ <meta name="viewport" content="width=device-width,initial-scale=1">
3286
+ <title>${pageTitle}</title>
3287
+ ${headTags.join('\n')}
3288
+ <style>
3289
+ *{box-sizing:border-box;margin:0;padding:0}
3290
+ ${KEYFRAMES}
3291
+ ${pseudoCSS.join('\n')}
3292
+ ${mediaCSSStr}
3293
+ ${cssBlocks.join('\n')}
3294
+ </style>
3295
+ </head>
3296
+ <body${bodyStyleStr?` style="${bodyStyleStr}"`:''}>
3297
+ ${bodyHtml}<script>(function(){
3298
+ ${evScript}
3299
+ ${jsBlocks.join('\n')}
3300
+ })()</script>
3301
+ </body>
3302
+ </html>`;
3303
+ }
3304
+
2734
3305
  // Saved web HTML for download
2735
3306
  let _lastWebHTML = '';
2736
3307
 
@@ -2931,7 +3502,7 @@ function showToast(msg = '✓ Saved') {
2931
3502
  // ── Update sidebar + filepath bar ───────────────────────────
2932
3503
  function setCurrentFile(fullPath, name) {
2933
3504
  currentFilePath = fullPath;
2934
- currentFile = name || fullPath.split(/[\\/]/).pop();
3505
+ currentFile = name || (fullPath ? fullPath.split(/[\\/]/).pop() : 'untitled.ss');
2935
3506
  document.getElementById('current-filename').textContent = currentFile;
2936
3507
  document.getElementById('fp-path').textContent = fullPath || '';
2937
3508
  document.getElementById('fp-path').title = fullPath || '';
@@ -2975,8 +3546,10 @@ async function doSave(filePath, toast = true) {
2975
3546
 
2976
3547
  // ── File operations (override playground versions) ──────────
2977
3548
  async function newFile() {
2978
- const name = prompt('File name:', 'untitled.ss') || 'untitled.ss';
2979
- const n = name.endsWith('.ss') ? name : name + '.ss';
3549
+ // prompt() is broken in Electron with contextIsolation
3550
+ let name = 'untitled.ss';
3551
+ if (!window.electronAPI) { name = prompt('File name:', 'untitled.ss') || 'untitled.ss'; }
3552
+ const n = name.endsWith('.ss') ? name : name + '.ss';
2980
3553
  const result = await api('/api/new', 'POST', { name: n });
2981
3554
  if (result.ok) {
2982
3555
  editor.value = result.content;
@@ -3242,21 +3815,27 @@ document.addEventListener('keydown', e => {
3242
3815
 
3243
3816
  // ── Init ────────────────────────────────────────────────────
3244
3817
  window.addEventListener('load', async () => {
3245
- // Check if a file was passed in the URL
3246
3818
  const params = new URLSearchParams(location.search);
3247
3819
  const initFile = params.get('file');
3248
3820
  if (initFile) {
3249
3821
  await openFilePath(initFile);
3250
3822
  } else {
3251
- // Load most recent file, or create untitled
3252
- const r = await api('/api/recent');
3253
- if (r.files && r.files.length) {
3254
- await openFilePath(r.files[0]);
3255
- } else {
3256
- await newFile();
3823
+ try {
3824
+ const r = await api('/api/recent');
3825
+ if (r && r.files && r.files.length) {
3826
+ await openFilePath(r.files[0]);
3827
+ } else {
3828
+ editor.value = EXAMPLES.hello;
3829
+ updateHighlight(); updateLineNumbers();
3830
+ setCurrentFile(null, 'untitled.ss');
3831
+ }
3832
+ } catch(e) {
3833
+ editor.value = EXAMPLES.hello;
3834
+ updateHighlight(); updateLineNumbers();
3835
+ setCurrentFile(null, 'untitled.ss');
3257
3836
  }
3258
3837
  }
3259
- refreshSidebar();
3838
+ try { refreshSidebar(); } catch(e) {}
3260
3839
  });
3261
3840
 
3262
3841
  // Hook editor input to markUnsaved (override plain updateEditor)