clou-lang 0.3.6 → 0.4.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.
@@ -60,6 +60,17 @@ const TokenType = {
60
60
  CODE: 'CODE',
61
61
  SLIDER: 'SLIDER',
62
62
  SUBMIT: 'SUBMIT',
63
+ COUNTDOWN: 'COUNTDOWN',
64
+ TYPING: 'TYPING',
65
+ CAROUSEL: 'CAROUSEL',
66
+ EMBED: 'EMBED',
67
+ BADGE: 'BADGE',
68
+ AVATAR: 'AVATAR',
69
+ STARS: 'STARS',
70
+ SEARCH: 'SEARCH',
71
+ TOAST: 'TOAST',
72
+ TOOLTIP: 'TOOLTIP',
73
+ DARKMODE: 'DARKMODE',
63
74
 
64
75
  // Keywords - Actions
65
76
  SHOW: 'SHOW',
@@ -147,6 +158,9 @@ const TokenType = {
147
158
  FUNCTION: 'FUNCTION',
148
159
  CALL: 'CALL',
149
160
  RETURN: 'RETURN',
161
+ RANDOM: 'RANDOM',
162
+ FETCH: 'FETCH',
163
+ MENU: 'MENU',
150
164
  LPAREN: 'LPAREN',
151
165
  RPAREN: 'RPAREN',
152
166
  COMMA: 'COMMA',
@@ -202,6 +216,17 @@ const KEYWORDS = {
202
216
  'code': TokenType.CODE,
203
217
  'slider': TokenType.SLIDER,
204
218
  'submit': TokenType.SUBMIT,
219
+ 'countdown': TokenType.COUNTDOWN,
220
+ 'typing': TokenType.TYPING,
221
+ 'carousel': TokenType.CAROUSEL,
222
+ 'embed': TokenType.EMBED,
223
+ 'badge': TokenType.BADGE,
224
+ 'avatar': TokenType.AVATAR,
225
+ 'stars': TokenType.STARS,
226
+ 'search': TokenType.SEARCH,
227
+ 'toast': TokenType.TOAST,
228
+ 'tooltip': TokenType.TOOLTIP,
229
+ 'darkmode': TokenType.DARKMODE,
205
230
  'show': TokenType.SHOW,
206
231
  'message': TokenType.MESSAGE,
207
232
  'hide': TokenType.HIDE,
@@ -281,6 +306,9 @@ const KEYWORDS = {
281
306
  'function': TokenType.FUNCTION,
282
307
  'call': TokenType.CALL,
283
308
  'return': TokenType.RETURN,
309
+ 'random': TokenType.RANDOM,
310
+ 'fetch': TokenType.FETCH,
311
+ 'menu': TokenType.MENU,
284
312
  'import': TokenType.IMPORT,
285
313
  'at': TokenType.AT,
286
314
  'to': TokenType.TO,
@@ -1149,6 +1177,17 @@ class Parser {
1149
1177
  case TokenType.CODE: return this.parseCode();
1150
1178
  case TokenType.SLIDER: return this.parseSlider();
1151
1179
  case TokenType.SUBMIT: return this.parseSubmit();
1180
+ case TokenType.COUNTDOWN: return this.parseCountdown();
1181
+ case TokenType.TYPING: return this.parseTyping();
1182
+ case TokenType.CAROUSEL: return this.parseCarousel();
1183
+ case TokenType.EMBED: return this.parseEmbed();
1184
+ case TokenType.BADGE: return this.parseBadge();
1185
+ case TokenType.AVATAR: return this.parseAvatar();
1186
+ case TokenType.STARS: return this.parseStars();
1187
+ case TokenType.SEARCH: return this.parseSearch();
1188
+ case TokenType.TOAST: return this.parseToast();
1189
+ case TokenType.TOOLTIP: return this.parseTooltip();
1190
+ case TokenType.DARKMODE: return this.parseDarkmode();
1152
1191
  case TokenType.SET: return this.parseSetVariable();
1153
1192
  case TokenType.USE: return this.parseUse();
1154
1193
  case TokenType.REPEAT: return this.parseRepeat();
@@ -1667,6 +1706,117 @@ class Parser {
1667
1706
  return { type: 'Submit', label };
1668
1707
  }
1669
1708
 
1709
+ // countdown "2026-12-31" OR countdown "2026-12-31" "Event Name"
1710
+ parseCountdown() {
1711
+ this.expect(TokenType.COUNTDOWN);
1712
+ const date = this.expect(TokenType.STRING).value;
1713
+ let label = null;
1714
+ if (this.peek() && this.peek().type === TokenType.STRING) {
1715
+ label = this.advance().value;
1716
+ }
1717
+ return { type: 'Countdown', date, label };
1718
+ }
1719
+
1720
+ // typing "Hello World"
1721
+ parseTyping() {
1722
+ this.expect(TokenType.TYPING);
1723
+ const value = this.expect(TokenType.STRING).value;
1724
+ return { type: 'Typing', value };
1725
+ }
1726
+
1727
+ // carousel:
1728
+ parseCarousel() {
1729
+ this.expect(TokenType.CAROUSEL);
1730
+ const node = { type: 'Carousel', children: [] };
1731
+
1732
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1733
+ this.advance();
1734
+ this.skipNewlines();
1735
+ node.children = this.parseBlock();
1736
+ }
1737
+
1738
+ return node;
1739
+ }
1740
+
1741
+ // embed "url"
1742
+ parseEmbed() {
1743
+ this.expect(TokenType.EMBED);
1744
+ const url = this.expect(TokenType.STRING).value;
1745
+ return { type: 'Embed', url };
1746
+ }
1747
+
1748
+ // badge "text"
1749
+ parseBadge() {
1750
+ this.expect(TokenType.BADGE);
1751
+ const value = this.expect(TokenType.STRING).value;
1752
+ const node = { type: 'Badge', value, children: [] };
1753
+
1754
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1755
+ this.advance();
1756
+ this.skipNewlines();
1757
+ node.children = this.parseBlock();
1758
+ }
1759
+
1760
+ return node;
1761
+ }
1762
+
1763
+ // avatar "url"
1764
+ parseAvatar() {
1765
+ this.expect(TokenType.AVATAR);
1766
+ const src = this.expect(TokenType.STRING).value;
1767
+ const node = { type: 'Avatar', src, children: [] };
1768
+
1769
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1770
+ this.advance();
1771
+ this.skipNewlines();
1772
+ node.children = this.parseBlock();
1773
+ }
1774
+
1775
+ return node;
1776
+ }
1777
+
1778
+ // stars 4 OR stars 4 "Rating"
1779
+ parseStars() {
1780
+ this.expect(TokenType.STARS);
1781
+ const value = this.expect(TokenType.NUMBER).value;
1782
+ let label = null;
1783
+ if (this.peek() && this.peek().type === TokenType.STRING) {
1784
+ label = this.advance().value;
1785
+ }
1786
+ return { type: 'Stars', value: parseInt(value, 10), label };
1787
+ }
1788
+
1789
+ // search "placeholder"
1790
+ parseSearch() {
1791
+ this.expect(TokenType.SEARCH);
1792
+ const placeholder = this.expect(TokenType.STRING).value;
1793
+ return { type: 'Search', placeholder };
1794
+ }
1795
+
1796
+ // toast "message" (as action inside button)
1797
+ parseToast() {
1798
+ this.expect(TokenType.TOAST);
1799
+ const value = this.expect(TokenType.STRING).value;
1800
+ return { type: 'Toast', value };
1801
+ }
1802
+
1803
+ // tooltip "text" "hover text"
1804
+ parseTooltip() {
1805
+ this.expect(TokenType.TOOLTIP);
1806
+ const text = this.expect(TokenType.STRING).value;
1807
+ let tip = '';
1808
+ if (this.peek() && this.peek().type === TokenType.STRING) {
1809
+ tip = this.advance().value;
1810
+ }
1811
+ return { type: 'Tooltip', text, tip };
1812
+ }
1813
+
1814
+ // darkmode
1815
+ parseDarkmode() {
1816
+ this.expect(TokenType.DARKMODE);
1817
+ return { type: 'Darkmode' };
1818
+ }
1819
+
1670
1820
  // button "text":
1671
1821
  parseButton() {
1672
1822
  this.expect(TokenType.BUTTON);
@@ -2070,6 +2220,14 @@ class Compiler {
2070
2220
  this.hasAccordion = false;
2071
2221
  this.hasProgress = false;
2072
2222
  this.hasSlider = false;
2223
+ this.hasCountdown = false;
2224
+ this.hasTyping = false;
2225
+ this.hasCarousel = false;
2226
+ this.hasToast = false;
2227
+ this.hasSearch = false;
2228
+ this.hasTooltip = false;
2229
+ this.hasDarkmode = false;
2230
+ this.hasStars = false;
2073
2231
  this.hoverStyles = [];
2074
2232
  this.variables = {};
2075
2233
  this.templates = {};
@@ -2133,6 +2291,14 @@ class Compiler {
2133
2291
  const accordionCSS = this.hasAccordion ? this.buildAccordionCSS() : '';
2134
2292
  const progressCSS = this.hasProgress ? this.buildProgressCSS() : '';
2135
2293
  const sliderCSS = this.hasSlider ? this.buildSliderCSS() : '';
2294
+ const countdownCSS = this.hasCountdown ? this.buildCountdownCSS() : '';
2295
+ const typingCSS = this.hasTyping ? this.buildTypingCSS() : '';
2296
+ const carouselCSS = this.hasCarousel ? this.buildCarouselCSS() : '';
2297
+ const toastCSS = this.hasToast ? this.buildToastCSS() : '';
2298
+ const searchCSS = this.hasSearch ? this.buildSearchCSS() : '';
2299
+ const tooltipCSS = this.hasTooltip ? this.buildTooltipCSS() : '';
2300
+ const darkmodeCSS = this.hasDarkmode ? this.buildDarkmodeCSS() : '';
2301
+ const starsCSS = this.hasStars ? this.buildStarsCSS() : '';
2136
2302
  const hoverCSS = this.hoverStyles.join('\n');
2137
2303
  const themeCSS = this.activeTheme ? `\n /* Theme: ${this.activeTheme.name} */\n${generateThemeCSS(this.activeTheme)}` : '';
2138
2304
 
@@ -2393,7 +2559,7 @@ class Compiler {
2393
2559
  opacity: 0.7;
2394
2560
  border-top: 1px solid rgba(128,128,128,0.2);
2395
2561
  }
2396
- ${navbarCSS}${modalCSS}${tabsCSS}${accordionCSS}${progressCSS}${sliderCSS}${animationCSS}${hoverCSS}${themeCSS}
2562
+ ${navbarCSS}${modalCSS}${tabsCSS}${accordionCSS}${progressCSS}${sliderCSS}${countdownCSS}${typingCSS}${carouselCSS}${toastCSS}${searchCSS}${tooltipCSS}${darkmodeCSS}${starsCSS}${animationCSS}${hoverCSS}${themeCSS}
2397
2563
  </style>
2398
2564
  </head>
2399
2565
  <body>
@@ -2542,6 +2708,92 @@ ${scripts}
2542
2708
  `;
2543
2709
  }
2544
2710
 
2711
+ buildCountdownCSS() {
2712
+ const accent = this.buttonColorCSS || '#4A90D9';
2713
+ return `
2714
+ .clou-countdown { display: flex; gap: 16px; justify-content: center; margin: 16px 0; flex-wrap: wrap; }
2715
+ .clou-countdown-label { width: 100%; text-align: center; font-size: 14px; opacity: 0.8; margin-bottom: 8px; }
2716
+ .clou-countdown-unit { display: flex; flex-direction: column; align-items: center; padding: 16px 20px; background: rgba(128,128,128,0.1); border-radius: 12px; min-width: 80px; }
2717
+ .clou-countdown-num { font-size: 2em; font-weight: 700; color: ${accent}; }
2718
+ .clou-countdown-text { font-size: 12px; opacity: 0.7; text-transform: uppercase; letter-spacing: 1px; }
2719
+ `;
2720
+ }
2721
+
2722
+ buildTypingCSS() {
2723
+ return `
2724
+ .clou-typing { overflow: hidden; border-right: 3px solid currentColor; white-space: nowrap; margin: 16px 0; font-size: 1.5em; font-weight: 600; animation: clou-typing-text 3s steps(40) 1s forwards, clou-typing-cursor 0.75s step-end infinite; width: 0; }
2725
+ @keyframes clou-typing-text { from { width: 0; } to { width: 100%; } }
2726
+ @keyframes clou-typing-cursor { 50% { border-color: transparent; } }
2727
+ `;
2728
+ }
2729
+
2730
+ buildCarouselCSS() {
2731
+ const accent = this.buttonColorCSS || '#4A90D9';
2732
+ return `
2733
+ .clou-carousel { position: relative; overflow: hidden; border-radius: 12px; margin: 16px 0; }
2734
+ .clou-carousel-track { display: flex; transition: transform 0.5s ease; }
2735
+ .clou-carousel-slide { min-width: 100%; }
2736
+ .clou-carousel-slide img { width: 100%; height: auto; display: block; }
2737
+ .clou-carousel-btn { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0,0,0,0.5); color: white; border: none; padding: 12px 16px; cursor: pointer; font-size: 18px; border-radius: 50%; z-index: 10; transition: background 0.2s; }
2738
+ .clou-carousel-btn:hover { background: ${accent}; transform: translateY(-50%) scale(1.05); box-shadow: none; }
2739
+ .clou-carousel-prev { left: 12px; }
2740
+ .clou-carousel-next { right: 12px; }
2741
+ .clou-carousel-dots { display: flex; justify-content: center; gap: 8px; padding: 12px 0; }
2742
+ .clou-carousel-dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(128,128,128,0.4); border: none; cursor: pointer; padding: 0; transition: background 0.2s; }
2743
+ .clou-carousel-dot:hover { transform: none; box-shadow: none; }
2744
+ .clou-carousel-dot.active { background: ${accent}; }
2745
+ `;
2746
+ }
2747
+
2748
+ buildToastCSS() {
2749
+ return `
2750
+ .clou-toast-container { position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 8px; }
2751
+ .clou-toast { padding: 14px 24px; background: #333; color: white; border-radius: 8px; font-size: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); animation: clou-toast-in 0.3s ease, clou-toast-out 0.3s ease 2.7s forwards; }
2752
+ @keyframes clou-toast-in { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } }
2753
+ @keyframes clou-toast-out { from { opacity: 1; } to { opacity: 0; transform: translateX(40px); } }
2754
+ `;
2755
+ }
2756
+
2757
+ buildSearchCSS() {
2758
+ const accent = this.buttonColorCSS || '#4A90D9';
2759
+ return `
2760
+ .clou-search { position: relative; margin: 12px 0; max-width: 500px; }
2761
+ .clou-search input { width: 100%; padding: 14px 20px 14px 48px; font-size: 16px; border: 2px solid rgba(128,128,128,0.2); border-radius: 50px; outline: none; background: rgba(255,255,255,0.1); color: inherit; transition: border-color 0.2s, box-shadow 0.2s; }
2762
+ .clou-search input:focus { border-color: ${accent}; box-shadow: 0 0 0 3px ${accent}33; }
2763
+ .clou-search-icon { position: absolute; left: 18px; top: 50%; transform: translateY(-50%); font-size: 18px; opacity: 0.5; pointer-events: none; }
2764
+ `;
2765
+ }
2766
+
2767
+ buildTooltipCSS() {
2768
+ return `
2769
+ .clou-tooltip { position: relative; display: inline-block; cursor: help; border-bottom: 1px dotted currentColor; }
2770
+ .clou-tooltip-text { visibility: hidden; background: #333; color: white; text-align: center; padding: 8px 14px; border-radius: 6px; font-size: 13px; position: absolute; z-index: 1000; bottom: 125%; left: 50%; transform: translateX(-50%); white-space: nowrap; opacity: 0; transition: opacity 0.2s; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
2771
+ .clou-tooltip-text::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #333 transparent transparent transparent; }
2772
+ .clou-tooltip:hover .clou-tooltip-text { visibility: visible; opacity: 1; }
2773
+ `;
2774
+ }
2775
+
2776
+ buildDarkmodeCSS() {
2777
+ return `
2778
+ .clou-darkmode-toggle { position: fixed; top: 16px; right: 16px; z-index: 9999; width: 48px; height: 48px; border-radius: 50%; border: 2px solid rgba(128,128,128,0.3); background: rgba(128,128,128,0.1); backdrop-filter: blur(8px); cursor: pointer; font-size: 22px; display: flex; align-items: center; justify-content: center; transition: background 0.3s, transform 0.2s; padding: 0; }
2779
+ .clou-darkmode-toggle:hover { background: rgba(128,128,128,0.2); transform: scale(1.1); box-shadow: none; }
2780
+ body.clou-light { background: #ffffff !important; color: #333333 !important; }
2781
+ body.clou-light .clou-card { background: rgba(0,0,0,0.05); }
2782
+ body.clou-light .clou-navbar { background: rgba(255,255,255,0.8); }
2783
+ body.clou-light input, body.clou-light textarea { background: rgba(0,0,0,0.05); border-color: rgba(0,0,0,0.15); color: #333; }
2784
+ `;
2785
+ }
2786
+
2787
+ buildStarsCSS() {
2788
+ return `
2789
+ .clou-stars { display: inline-flex; gap: 4px; margin: 8px 0; align-items: center; }
2790
+ .clou-star { font-size: 1.4em; }
2791
+ .clou-star.filled { color: #FFD700; }
2792
+ .clou-star.empty { color: rgba(128,128,128,0.3); }
2793
+ .clou-stars-label { margin-left: 8px; font-size: 14px; opacity: 0.7; }
2794
+ `;
2795
+ }
2796
+
2545
2797
  buildAnimationCSS() {
2546
2798
  let css = '';
2547
2799
 
@@ -2596,6 +2848,52 @@ ${scripts}
2596
2848
  `;
2597
2849
  }
2598
2850
 
2851
+ if (this.animations.has('pulse')) {
2852
+ css += `
2853
+ @keyframes clou-pulse {
2854
+ 0%, 100% { transform: scale(1); opacity: 1; }
2855
+ 50% { transform: scale(1.05); opacity: 0.8; }
2856
+ }
2857
+ .clou-animate-pulse { animation: clou-pulse 2s ease infinite; }
2858
+ `;
2859
+ }
2860
+
2861
+ if (this.animations.has('shake')) {
2862
+ css += `
2863
+ @keyframes clou-shake {
2864
+ 0%, 100% { transform: translateX(0); }
2865
+ 20% { transform: translateX(-8px); }
2866
+ 40% { transform: translateX(8px); }
2867
+ 60% { transform: translateX(-4px); }
2868
+ 80% { transform: translateX(4px); }
2869
+ }
2870
+ .clou-animate-shake { animation: clou-shake 0.6s ease; }
2871
+ `;
2872
+ }
2873
+
2874
+ if (this.animations.has('float')) {
2875
+ css += `
2876
+ @keyframes clou-float {
2877
+ 0%, 100% { transform: translateY(0); }
2878
+ 50% { transform: translateY(-10px); }
2879
+ }
2880
+ .clou-animate-float { animation: clou-float 3s ease-in-out infinite; }
2881
+ `;
2882
+ }
2883
+
2884
+ if (this.animations.has('typewrite')) {
2885
+ css += `
2886
+ @keyframes clou-typewrite {
2887
+ from { width: 0; }
2888
+ to { width: 100%; }
2889
+ }
2890
+ @keyframes clou-blink {
2891
+ 50% { border-color: transparent; }
2892
+ }
2893
+ .clou-animate-typewrite { overflow: hidden; white-space: nowrap; border-right: 3px solid currentColor; animation: clou-typewrite 3s steps(40) forwards, clou-blink 0.75s step-end infinite; }
2894
+ `;
2895
+ }
2896
+
2599
2897
  return css;
2600
2898
  }
2601
2899
 
@@ -2889,6 +3187,137 @@ ${scripts}
2889
3187
  case 'Submit':
2890
3188
  return ` <button type="submit">${this.escapeHtml(this.interpolate(node.label))}</button>`;
2891
3189
 
3190
+ case 'Countdown': {
3191
+ this.hasCountdown = true;
3192
+ const cdId = this.uid();
3193
+ const label = node.label ? `<div class="clou-countdown-label">${this.escapeHtml(this.interpolate(node.label))}</div>` : '';
3194
+ this.scripts.push(
3195
+ `(function() {\n` +
3196
+ ` var target = new Date(${JSON.stringify(node.date)}).getTime();\n` +
3197
+ ` var el = document.getElementById('${cdId}');\n` +
3198
+ ` function update() {\n` +
3199
+ ` var now = Date.now();\n` +
3200
+ ` var diff = Math.max(0, target - now);\n` +
3201
+ ` var d = Math.floor(diff / 86400000);\n` +
3202
+ ` var h = Math.floor((diff % 86400000) / 3600000);\n` +
3203
+ ` var m = Math.floor((diff % 3600000) / 60000);\n` +
3204
+ ` var s = Math.floor((diff % 60000) / 1000);\n` +
3205
+ ` el.querySelector('.cd-d').textContent = d;\n` +
3206
+ ` el.querySelector('.cd-h').textContent = h;\n` +
3207
+ ` el.querySelector('.cd-m').textContent = m;\n` +
3208
+ ` el.querySelector('.cd-s').textContent = s;\n` +
3209
+ ` if (diff > 0) setTimeout(update, 1000);\n` +
3210
+ ` }\n` +
3211
+ ` update();\n` +
3212
+ `})();`
3213
+ );
3214
+ return ` <div class="clou-countdown" id="${cdId}">\n ${label}\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-d">0</span><span class="clou-countdown-text">Days</span></div>\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-h">0</span><span class="clou-countdown-text">Hours</span></div>\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-m">0</span><span class="clou-countdown-text">Min</span></div>\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-s">0</span><span class="clou-countdown-text">Sec</span></div>\n </div>`;
3215
+ }
3216
+
3217
+ case 'Typing': {
3218
+ this.hasTyping = true;
3219
+ return ` <div class="clou-typing">${this.escapeHtml(this.interpolate(node.value))}</div>`;
3220
+ }
3221
+
3222
+ case 'Carousel': {
3223
+ this.hasCarousel = true;
3224
+ const carId = this.uid();
3225
+ const slides = (node.children || []).filter(c => c.type === 'Image' || c.type === 'Card');
3226
+ let slidesHtml = '';
3227
+ slides.forEach((slide, i) => {
3228
+ slidesHtml += `\n <div class="clou-carousel-slide">${this.compileNode(slide)}</div>`;
3229
+ });
3230
+ let dotsHtml = '';
3231
+ slides.forEach((_, i) => {
3232
+ dotsHtml += `<button class="clou-carousel-dot${i === 0 ? ' active' : ''}" data-idx="${i}"></button>`;
3233
+ });
3234
+ this.scripts.push(
3235
+ `(function() {\n` +
3236
+ ` var el = document.getElementById('${carId}');\n` +
3237
+ ` var track = el.querySelector('.clou-carousel-track');\n` +
3238
+ ` var dots = el.querySelectorAll('.clou-carousel-dot');\n` +
3239
+ ` var total = ${slides.length};\n` +
3240
+ ` var idx = 0;\n` +
3241
+ ` function go(n) { idx = (n + total) % total; track.style.transform = 'translateX(-' + (idx * 100) + '%)'; dots.forEach(function(d,i) { d.classList.toggle('active', i === idx); }); }\n` +
3242
+ ` el.querySelector('.clou-carousel-prev').onclick = function() { go(idx - 1); };\n` +
3243
+ ` el.querySelector('.clou-carousel-next').onclick = function() { go(idx + 1); };\n` +
3244
+ ` dots.forEach(function(d) { d.onclick = function() { go(parseInt(d.dataset.idx)); }; });\n` +
3245
+ `})();`
3246
+ );
3247
+ return ` <div class="clou-carousel" id="${carId}">\n <div class="clou-carousel-track">${slidesHtml}\n </div>\n <button class="clou-carousel-btn clou-carousel-prev">&#10094;</button>\n <button class="clou-carousel-btn clou-carousel-next">&#10095;</button>\n <div class="clou-carousel-dots">${dotsHtml}</div>\n </div>`;
3248
+ }
3249
+
3250
+ case 'Embed': {
3251
+ const url = this.interpolate(node.url);
3252
+ // Convert YouTube watch URLs to embed
3253
+ let embedUrl = url;
3254
+ if (url.includes('youtube.com/watch')) {
3255
+ const vid = url.split('v=')[1];
3256
+ if (vid) embedUrl = 'https://www.youtube.com/embed/' + vid.split('&')[0];
3257
+ } else if (url.includes('youtu.be/')) {
3258
+ embedUrl = 'https://www.youtube.com/embed/' + url.split('youtu.be/')[1];
3259
+ }
3260
+ return ` <iframe src="${this.escapeHtml(embedUrl)}" style="width: 100%; height: 400px; border: none; border-radius: 12px; margin: 12px 0;" allowfullscreen></iframe>`;
3261
+ }
3262
+
3263
+ case 'Badge': {
3264
+ const style = this.extractInlineStyles(node.children || []);
3265
+ const accent = this.buttonColorCSS || '#4A90D9';
3266
+ const styleStr = style ? style + '; ' : '';
3267
+ return ` <span style="${styleStr}display: inline-block; padding: 4px 12px; font-size: 12px; font-weight: 600; border-radius: 20px; background: ${accent}; color: white; text-transform: uppercase; letter-spacing: 0.5px;">${this.escapeHtml(this.interpolate(node.value))}</span>`;
3268
+ }
3269
+
3270
+ case 'Avatar': {
3271
+ const style = this.extractInlineStyles(node.children || []);
3272
+ const styleStr = style ? style + '; ' : '';
3273
+ return ` <img src="${this.escapeHtml(this.interpolate(node.src))}" alt="avatar" style="${styleStr}width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin: 8px 0;">`;
3274
+ }
3275
+
3276
+ case 'Stars': {
3277
+ this.hasStars = true;
3278
+ const count = Math.min(Math.max(node.value, 0), 5);
3279
+ let starsHtml = '';
3280
+ for (let i = 0; i < 5; i++) {
3281
+ starsHtml += `<span class="clou-star ${i < count ? 'filled' : 'empty'}">${i < count ? '\u2605' : '\u2606'}</span>`;
3282
+ }
3283
+ const label = node.label ? `<span class="clou-stars-label">${this.escapeHtml(this.interpolate(node.label))}</span>` : '';
3284
+ return ` <div class="clou-stars">${starsHtml}${label}</div>`;
3285
+ }
3286
+
3287
+ case 'Search': {
3288
+ this.hasSearch = true;
3289
+ const id = this.uid();
3290
+ return ` <div class="clou-search">\n <span class="clou-search-icon">\u{1F50D}</span>\n <input id="${id}" type="text" placeholder="${this.escapeHtml(this.interpolate(node.placeholder))}">\n </div>`;
3291
+ }
3292
+
3293
+ case 'Toast': {
3294
+ // Toast as action (inside button)
3295
+ this.hasToast = true;
3296
+ return '';
3297
+ }
3298
+
3299
+ case 'Tooltip': {
3300
+ this.hasTooltip = true;
3301
+ return ` <span class="clou-tooltip">${this.escapeHtml(this.interpolate(node.text))}<span class="clou-tooltip-text">${this.escapeHtml(this.interpolate(node.tip))}</span></span>`;
3302
+ }
3303
+
3304
+ case 'Darkmode': {
3305
+ this.hasDarkmode = true;
3306
+ const dmId = this.uid();
3307
+ this.scripts.push(
3308
+ `(function() {\n` +
3309
+ ` var btn = document.getElementById('${dmId}');\n` +
3310
+ ` var isDark = true;\n` +
3311
+ ` btn.onclick = function() {\n` +
3312
+ ` isDark = !isDark;\n` +
3313
+ ` document.body.classList.toggle('clou-light', !isDark);\n` +
3314
+ ` btn.textContent = isDark ? '\\u263E' : '\\u2600';\n` +
3315
+ ` };\n` +
3316
+ `})();`
3317
+ );
3318
+ return ` <button class="clou-darkmode-toggle" id="${dmId}">\u263E</button>`;
3319
+ }
3320
+
2892
3321
  case 'Icon':
2893
3322
  return ` <span class="clou-icon">${node.value}</span>`;
2894
3323
 
@@ -2995,6 +3424,9 @@ ${scripts}
2995
3424
  return `document.getElementById(${JSON.stringify(node.target)} + '-overlay').classList.remove('active');`;
2996
3425
  case 'GoTo':
2997
3426
  return `window.location.href = ${JSON.stringify(node.url)};`;
3427
+ case 'Toast':
3428
+ this.hasToast = true;
3429
+ return `(function() { var c = document.querySelector('.clou-toast-container'); if (!c) { c = document.createElement('div'); c.className = 'clou-toast-container'; document.body.appendChild(c); } var t = document.createElement('div'); t.className = 'clou-toast'; t.textContent = ${JSON.stringify(this.interpolate(node.value))}; c.appendChild(t); setTimeout(function() { t.remove(); }, 3000); })();`;
2998
3430
  default:
2999
3431
  return '';
3000
3432
  }
@@ -3041,7 +3473,7 @@ ${scripts}
3041
3473
  }
3042
3474
 
3043
3475
  isAction(node) {
3044
- return ['ShowMessage', 'Show', 'Hide', 'Toggle', 'GoTo', 'Open', 'Close'].includes(node.type);
3476
+ return ['ShowMessage', 'Show', 'Hide', 'Toggle', 'GoTo', 'Open', 'Close', 'Toast'].includes(node.type);
3045
3477
  }
3046
3478
 
3047
3479
  // Filter only action nodes
@@ -228,7 +228,7 @@ body {
228
228
  <body>
229
229
 
230
230
  <div class="header">
231
- <div class="logo">Clou Playground <span>v0.2</span></div>
231
+ <div class="logo">Clou Playground <span>v0.4</span></div>
232
232
  <div class="header-actions">
233
233
  <select class="theme-select" id="themeSelect">
234
234
  <option value="">No Theme</option>
@@ -252,7 +252,7 @@ body {
252
252
  <div class="resize-handle" id="resizeHandle"></div>
253
253
  <div class="preview-panel">
254
254
  <div class="panel-header">Preview</div>
255
- <iframe id="preview" sandbox="allow-scripts allow-modals"></iframe>
255
+ <iframe id="preview" sandbox="allow-scripts allow-modals allow-same-origin"></iframe>
256
256
  </div>
257
257
  </div>
258
258
 
@@ -660,11 +660,8 @@ page "{name}'s Portfolio":
660
660
  const result = Clou.buildClou(source);
661
661
  lastHtml = result.html;
662
662
 
663
- // Write to iframe
664
- const doc = preview.contentDocument || preview.contentWindow.document;
665
- doc.open();
666
- doc.write(lastHtml);
667
- doc.close();
663
+ // Write to iframe using srcdoc (avoids cross-origin issues)
664
+ preview.srcdoc = lastHtml;
668
665
 
669
666
  status.textContent = 'Compiled';
670
667
  status.className = 'status status-ok';