clou-lang 0.3.1 → 0.3.6

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.
@@ -45,6 +45,21 @@ const TokenType = {
45
45
  SECTION: 'SECTION',
46
46
  SPACE: 'SPACE',
47
47
  COLUMNS: 'COLUMNS',
48
+ FORM: 'FORM',
49
+ TABLE: 'TABLE',
50
+ TABS: 'TABS',
51
+ TAB: 'TAB',
52
+ ACCORDION: 'ACCORDION',
53
+ PANEL: 'PANEL',
54
+ PROGRESS: 'PROGRESS',
55
+ DROPDOWN: 'DROPDOWN',
56
+ OPTION: 'OPTION',
57
+ TEXTAREA: 'TEXTAREA',
58
+ CHECKBOX: 'CHECKBOX',
59
+ AUDIO: 'AUDIO',
60
+ CODE: 'CODE',
61
+ SLIDER: 'SLIDER',
62
+ SUBMIT: 'SUBMIT',
48
63
 
49
64
  // Keywords - Actions
50
65
  SHOW: 'SHOW',
@@ -136,6 +151,10 @@ const TokenType = {
136
151
  RPAREN: 'RPAREN',
137
152
  COMMA: 'COMMA',
138
153
 
154
+ // Keywords - Imports & Routing
155
+ IMPORT: 'IMPORT',
156
+ AT: 'AT',
157
+
139
158
  // Keywords - Connectors
140
159
  TO: 'TO',
141
160
  AND: 'AND',
@@ -168,6 +187,21 @@ const KEYWORDS = {
168
187
  'section': TokenType.SECTION,
169
188
  'space': TokenType.SPACE,
170
189
  'columns': TokenType.COLUMNS,
190
+ 'form': TokenType.FORM,
191
+ 'table': TokenType.TABLE,
192
+ 'tabs': TokenType.TABS,
193
+ 'tab': TokenType.TAB,
194
+ 'accordion': TokenType.ACCORDION,
195
+ 'panel': TokenType.PANEL,
196
+ 'progress': TokenType.PROGRESS,
197
+ 'dropdown': TokenType.DROPDOWN,
198
+ 'option': TokenType.OPTION,
199
+ 'textarea': TokenType.TEXTAREA,
200
+ 'checkbox': TokenType.CHECKBOX,
201
+ 'audio': TokenType.AUDIO,
202
+ 'code': TokenType.CODE,
203
+ 'slider': TokenType.SLIDER,
204
+ 'submit': TokenType.SUBMIT,
171
205
  'show': TokenType.SHOW,
172
206
  'message': TokenType.MESSAGE,
173
207
  'hide': TokenType.HIDE,
@@ -247,6 +281,8 @@ const KEYWORDS = {
247
281
  'function': TokenType.FUNCTION,
248
282
  'call': TokenType.CALL,
249
283
  'return': TokenType.RETURN,
284
+ 'import': TokenType.IMPORT,
285
+ 'at': TokenType.AT,
250
286
  'to': TokenType.TO,
251
287
  'and': TokenType.AND,
252
288
  };
@@ -368,6 +404,15 @@ function tokenize(source) {
368
404
  continue;
369
405
  }
370
406
 
407
+ // Math operators: +, -, *, / (but not // which is a comment, and not - in words)
408
+ if ((ch === '+' || ch === '*') ||
409
+ (ch === '-' && col + 1 < line.length && line[col + 1] === ' ') ||
410
+ (ch === '/' && col + 1 < line.length && line[col + 1] !== '/')) {
411
+ tokens.push(new Token(TokenType.IDENTIFIER, ch, lineNum + 1, col + 1));
412
+ col++;
413
+ continue;
414
+ }
415
+
371
416
  // String literal (double or single quotes)
372
417
  if (ch === '"' || ch === "'") {
373
418
  const quote = ch;
@@ -995,7 +1040,12 @@ class Parser {
995
1040
  this.skipNewlines();
996
1041
  if (!this.peek() || this.peek().type === TokenType.EOF) break;
997
1042
 
998
- if (this.peek().type === TokenType.TEMPLATE) {
1043
+ if (this.peek().type === TokenType.IMPORT) {
1044
+ // imports are handled at a higher level, store as-is
1045
+ const imp = this.parseImport();
1046
+ if (!this.ast_imports) this.ast_imports = [];
1047
+ this.ast_imports.push(imp);
1048
+ } else if (this.peek().type === TokenType.TEMPLATE) {
999
1049
  templates.push(this.parseTemplate());
1000
1050
  } else if (this.peek().type === TokenType.SET) {
1001
1051
  variables.push(this.parseSetVariable());
@@ -1013,18 +1063,31 @@ class Parser {
1013
1063
  this.skipNewlines();
1014
1064
  }
1015
1065
 
1016
- return { type: 'Program', pages, styles, templates, variables };
1066
+ return { type: 'Program', pages, styles, templates, variables, imports: this.ast_imports || [] };
1017
1067
  }
1018
1068
 
1019
- // page "Title":
1069
+ // page "Title": OR page "Title" at "/path":
1020
1070
  parsePage() {
1021
1071
  this.expect(TokenType.PAGE);
1022
1072
  const title = this.expect(TokenType.STRING).value;
1073
+ let route = null;
1074
+
1075
+ if (this.match(TokenType.AT)) {
1076
+ route = this.expect(TokenType.STRING).value;
1077
+ }
1078
+
1023
1079
  this.expect(TokenType.COLON);
1024
1080
  this.skipNewlines();
1025
1081
 
1026
1082
  const children = this.parseBlock();
1027
- return { type: 'Page', title, children };
1083
+ return { type: 'Page', title, route, children };
1084
+ }
1085
+
1086
+ // import "file.clou"
1087
+ parseImport() {
1088
+ this.expect(TokenType.IMPORT);
1089
+ const file = this.expect(TokenType.STRING).value;
1090
+ return { type: 'Import', file };
1028
1091
  }
1029
1092
 
1030
1093
  // Parse an indented block of elements
@@ -1071,6 +1134,21 @@ class Parser {
1071
1134
  case TokenType.GRID: return this.parseGrid();
1072
1135
  case TokenType.SECTION: return this.parseSection();
1073
1136
  case TokenType.SPACE: return this.parseSpace();
1137
+ case TokenType.FORM: return this.parseForm();
1138
+ case TokenType.TABLE: return this.parseTable();
1139
+ case TokenType.TABS: return this.parseTabs();
1140
+ case TokenType.TAB: return this.parseTab();
1141
+ case TokenType.ACCORDION: return this.parseAccordion();
1142
+ case TokenType.PANEL: return this.parsePanel();
1143
+ case TokenType.PROGRESS: return this.parseProgress();
1144
+ case TokenType.DROPDOWN: return this.parseDropdown();
1145
+ case TokenType.OPTION: return this.parseOption();
1146
+ case TokenType.TEXTAREA: return this.parseTextarea();
1147
+ case TokenType.CHECKBOX: return this.parseCheckbox();
1148
+ case TokenType.AUDIO: return this.parseAudio();
1149
+ case TokenType.CODE: return this.parseCode();
1150
+ case TokenType.SLIDER: return this.parseSlider();
1151
+ case TokenType.SUBMIT: return this.parseSubmit();
1074
1152
  case TokenType.SET: return this.parseSetVariable();
1075
1153
  case TokenType.USE: return this.parseUse();
1076
1154
  case TokenType.REPEAT: return this.parseRepeat();
@@ -1410,6 +1488,185 @@ class Parser {
1410
1488
  return { type: 'Space', value };
1411
1489
  }
1412
1490
 
1491
+ // form:
1492
+ parseForm() {
1493
+ this.expect(TokenType.FORM);
1494
+ const node = { type: 'Form', action: null, children: [] };
1495
+
1496
+ if (this.peek() && this.peek().type === TokenType.STRING) {
1497
+ node.action = this.advance().value;
1498
+ }
1499
+
1500
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1501
+ this.advance();
1502
+ this.skipNewlines();
1503
+ node.children = this.parseBlock();
1504
+ }
1505
+
1506
+ return node;
1507
+ }
1508
+
1509
+ // table:
1510
+ parseTable() {
1511
+ this.expect(TokenType.TABLE);
1512
+ const node = { type: 'Table', children: [] };
1513
+
1514
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1515
+ this.advance();
1516
+ this.skipNewlines();
1517
+ node.children = this.parseBlock();
1518
+ }
1519
+
1520
+ return node;
1521
+ }
1522
+
1523
+ // tabs:
1524
+ parseTabs() {
1525
+ this.expect(TokenType.TABS);
1526
+ const node = { type: 'Tabs', children: [] };
1527
+
1528
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1529
+ this.advance();
1530
+ this.skipNewlines();
1531
+ node.children = this.parseBlock();
1532
+ }
1533
+
1534
+ return node;
1535
+ }
1536
+
1537
+ // tab "name":
1538
+ parseTab() {
1539
+ this.expect(TokenType.TAB);
1540
+ const name = this.expect(TokenType.STRING).value;
1541
+ const node = { type: 'Tab', name, children: [] };
1542
+
1543
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1544
+ this.advance();
1545
+ this.skipNewlines();
1546
+ node.children = this.parseBlock();
1547
+ }
1548
+
1549
+ return node;
1550
+ }
1551
+
1552
+ // accordion:
1553
+ parseAccordion() {
1554
+ this.expect(TokenType.ACCORDION);
1555
+ const node = { type: 'Accordion', children: [] };
1556
+
1557
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1558
+ this.advance();
1559
+ this.skipNewlines();
1560
+ node.children = this.parseBlock();
1561
+ }
1562
+
1563
+ return node;
1564
+ }
1565
+
1566
+ // panel "title":
1567
+ parsePanel() {
1568
+ this.expect(TokenType.PANEL);
1569
+ const name = this.expect(TokenType.STRING).value;
1570
+ const node = { type: 'Panel', name, children: [] };
1571
+
1572
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1573
+ this.advance();
1574
+ this.skipNewlines();
1575
+ node.children = this.parseBlock();
1576
+ }
1577
+
1578
+ return node;
1579
+ }
1580
+
1581
+ // progress 75 OR progress 75 "Label"
1582
+ parseProgress() {
1583
+ this.expect(TokenType.PROGRESS);
1584
+ const value = this.expect(TokenType.NUMBER).value;
1585
+ let label = null;
1586
+ if (this.peek() && this.peek().type === TokenType.STRING) {
1587
+ label = this.advance().value;
1588
+ }
1589
+ return { type: 'Progress', value, label };
1590
+ }
1591
+
1592
+ // dropdown "label":
1593
+ parseDropdown() {
1594
+ this.expect(TokenType.DROPDOWN);
1595
+ const label = this.expect(TokenType.STRING).value;
1596
+ const node = { type: 'Dropdown', label, children: [] };
1597
+
1598
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1599
+ this.advance();
1600
+ this.skipNewlines();
1601
+ node.children = this.parseBlock();
1602
+ }
1603
+
1604
+ return node;
1605
+ }
1606
+
1607
+ // option "value"
1608
+ parseOption() {
1609
+ this.expect(TokenType.OPTION);
1610
+ const value = this.expect(TokenType.STRING).value;
1611
+ return { type: 'Option', value };
1612
+ }
1613
+
1614
+ // textarea "placeholder"
1615
+ parseTextarea() {
1616
+ this.expect(TokenType.TEXTAREA);
1617
+ const placeholder = this.expect(TokenType.STRING).value;
1618
+ const node = { type: 'Textarea', placeholder, children: [] };
1619
+
1620
+ if (this.peek() && this.peek().type === TokenType.COLON) {
1621
+ this.advance();
1622
+ this.skipNewlines();
1623
+ node.children = this.parseBlock();
1624
+ }
1625
+
1626
+ return node;
1627
+ }
1628
+
1629
+ // checkbox "label"
1630
+ parseCheckbox() {
1631
+ this.expect(TokenType.CHECKBOX);
1632
+ const label = this.expect(TokenType.STRING).value;
1633
+ return { type: 'Checkbox', label };
1634
+ }
1635
+
1636
+ // audio "src"
1637
+ parseAudio() {
1638
+ this.expect(TokenType.AUDIO);
1639
+ const src = this.expect(TokenType.STRING).value;
1640
+ return { type: 'Audio', src };
1641
+ }
1642
+
1643
+ // code "content"
1644
+ parseCode() {
1645
+ this.expect(TokenType.CODE);
1646
+ const value = this.expect(TokenType.STRING).value;
1647
+ return { type: 'Code', value };
1648
+ }
1649
+
1650
+ // slider "label" 0 to 100
1651
+ parseSlider() {
1652
+ this.expect(TokenType.SLIDER);
1653
+ const label = this.expect(TokenType.STRING).value;
1654
+ let min = '0', max = '100';
1655
+ if (this.peek() && this.peek().type === TokenType.NUMBER) {
1656
+ min = this.advance().value;
1657
+ this.expect(TokenType.TO);
1658
+ max = this.expect(TokenType.NUMBER).value;
1659
+ }
1660
+ return { type: 'Slider', label, min, max };
1661
+ }
1662
+
1663
+ // submit "text"
1664
+ parseSubmit() {
1665
+ this.expect(TokenType.SUBMIT);
1666
+ const label = this.expect(TokenType.STRING).value;
1667
+ return { type: 'Submit', label };
1668
+ }
1669
+
1413
1670
  // button "text":
1414
1671
  parseButton() {
1415
1672
  this.expect(TokenType.BUTTON);
@@ -1809,6 +2066,10 @@ class Compiler {
1809
2066
  this.animations = new Set();
1810
2067
  this.hasModal = false;
1811
2068
  this.hasNavbar = false;
2069
+ this.hasTabs = false;
2070
+ this.hasAccordion = false;
2071
+ this.hasProgress = false;
2072
+ this.hasSlider = false;
1812
2073
  this.hoverStyles = [];
1813
2074
  this.variables = {};
1814
2075
  this.templates = {};
@@ -1868,6 +2129,10 @@ class Compiler {
1868
2129
  const animationCSS = this.buildAnimationCSS();
1869
2130
  const modalCSS = this.hasModal ? this.buildModalCSS() : '';
1870
2131
  const navbarCSS = this.hasNavbar ? this.buildNavbarCSS() : '';
2132
+ const tabsCSS = this.hasTabs ? this.buildTabsCSS() : '';
2133
+ const accordionCSS = this.hasAccordion ? this.buildAccordionCSS() : '';
2134
+ const progressCSS = this.hasProgress ? this.buildProgressCSS() : '';
2135
+ const sliderCSS = this.hasSlider ? this.buildSliderCSS() : '';
1871
2136
  const hoverCSS = this.hoverStyles.join('\n');
1872
2137
  const themeCSS = this.activeTheme ? `\n /* Theme: ${this.activeTheme.name} */\n${generateThemeCSS(this.activeTheme)}` : '';
1873
2138
 
@@ -2045,6 +2310,82 @@ class Compiler {
2045
2310
  display: inline-block;
2046
2311
  }
2047
2312
 
2313
+ .clou-table {
2314
+ width: 100%;
2315
+ border-collapse: collapse;
2316
+ margin: 16px 0;
2317
+ }
2318
+
2319
+ .clou-table th, .clou-table td {
2320
+ padding: 12px 16px;
2321
+ text-align: left;
2322
+ border-bottom: 1px solid rgba(128,128,128,0.2);
2323
+ }
2324
+
2325
+ .clou-table th {
2326
+ font-weight: 600;
2327
+ opacity: 0.8;
2328
+ }
2329
+
2330
+ .clou-table tr:hover td {
2331
+ background: rgba(128,128,128,0.05);
2332
+ }
2333
+
2334
+ .clou-form {
2335
+ margin: 16px 0;
2336
+ display: flex;
2337
+ flex-direction: column;
2338
+ gap: 12px;
2339
+ }
2340
+
2341
+ .clou-checkbox {
2342
+ display: flex;
2343
+ align-items: center;
2344
+ gap: 8px;
2345
+ margin: 8px 0;
2346
+ cursor: pointer;
2347
+ }
2348
+
2349
+ .clou-checkbox input[type="checkbox"] {
2350
+ width: 18px;
2351
+ height: 18px;
2352
+ cursor: pointer;
2353
+ }
2354
+
2355
+ .clou-code {
2356
+ background: rgba(0,0,0,0.3);
2357
+ border-radius: 8px;
2358
+ padding: 16px 20px;
2359
+ margin: 12px 0;
2360
+ overflow-x: auto;
2361
+ font-family: 'Fira Code', 'Consolas', monospace;
2362
+ font-size: 14px;
2363
+ line-height: 1.5;
2364
+ }
2365
+
2366
+ .clou-audio {
2367
+ width: 100%;
2368
+ max-width: 400px;
2369
+ margin: 8px 0;
2370
+ }
2371
+
2372
+ .clou-dropdown {
2373
+ margin: 8px 0;
2374
+ }
2375
+
2376
+ .clou-dropdown label {
2377
+ display: block;
2378
+ margin-bottom: 6px;
2379
+ font-size: 14px;
2380
+ opacity: 0.8;
2381
+ }
2382
+
2383
+ select.clou-input {
2384
+ width: auto;
2385
+ min-width: 200px;
2386
+ cursor: pointer;
2387
+ }
2388
+
2048
2389
  .clou-footer {
2049
2390
  padding: 40px 20px;
2050
2391
  margin-top: 40px;
@@ -2052,7 +2393,7 @@ class Compiler {
2052
2393
  opacity: 0.7;
2053
2394
  border-top: 1px solid rgba(128,128,128,0.2);
2054
2395
  }
2055
- ${navbarCSS}${modalCSS}${animationCSS}${hoverCSS}${themeCSS}
2396
+ ${navbarCSS}${modalCSS}${tabsCSS}${accordionCSS}${progressCSS}${sliderCSS}${animationCSS}${hoverCSS}${themeCSS}
2056
2397
  </style>
2057
2398
  </head>
2058
2399
  <body>
@@ -2157,6 +2498,50 @@ ${scripts}
2157
2498
  `;
2158
2499
  }
2159
2500
 
2501
+ buildTabsCSS() {
2502
+ return `
2503
+ .clou-tabs { margin: 16px 0; }
2504
+ .clou-tab-buttons { display: flex; gap: 0; border-bottom: 2px solid rgba(128,128,128,0.2); margin-bottom: 16px; }
2505
+ .clou-tab-btn { padding: 10px 24px; background: none; border: none; color: inherit; cursor: pointer; font-size: 16px; font-weight: 500; opacity: 0.6; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: opacity 0.2s, border-color 0.2s; }
2506
+ .clou-tab-btn:hover { opacity: 0.9; transform: none; box-shadow: none; }
2507
+ .clou-tab-btn.active { opacity: 1; border-bottom-color: currentColor; }
2508
+ .clou-tab-panel { display: none; }
2509
+ .clou-tab-panel.active { display: block; }
2510
+ `;
2511
+ }
2512
+
2513
+ buildAccordionCSS() {
2514
+ return `
2515
+ .clou-accordion { margin: 16px 0; }
2516
+ .clou-panel { border: 1px solid rgba(128,128,128,0.2); border-radius: 8px; margin: 8px 0; overflow: hidden; }
2517
+ .clou-panel-header { padding: 16px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-weight: 600; transition: background 0.2s; }
2518
+ .clou-panel-header:hover { background: rgba(128,128,128,0.1); }
2519
+ .clou-panel-arrow { transition: transform 0.3s; font-size: 12px; }
2520
+ .clou-panel.open .clou-panel-arrow { transform: rotate(180deg); }
2521
+ .clou-panel-body { max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
2522
+ .clou-panel.open .clou-panel-body { max-height: 500px; padding: 0 20px 16px; }
2523
+ `;
2524
+ }
2525
+
2526
+ buildProgressCSS() {
2527
+ const accent = this.buttonColorCSS || '#4A90D9';
2528
+ return `
2529
+ .clou-progress { margin: 12px 0; }
2530
+ .clou-progress-label { font-size: 14px; margin-bottom: 6px; opacity: 0.8; }
2531
+ .clou-progress-bar { width: 100%; height: 12px; background: rgba(128,128,128,0.2); border-radius: 6px; overflow: hidden; }
2532
+ .clou-progress-fill { height: 100%; background: ${accent}; border-radius: 6px; transition: width 0.6s ease; }
2533
+ `;
2534
+ }
2535
+
2536
+ buildSliderCSS() {
2537
+ const accent = this.buttonColorCSS || '#4A90D9';
2538
+ return `
2539
+ .clou-slider { margin: 12px 0; }
2540
+ .clou-slider label { display: block; font-size: 14px; margin-bottom: 6px; }
2541
+ .clou-slider input[type="range"] { width: 100%; max-width: 400px; accent-color: ${accent}; }
2542
+ `;
2543
+ }
2544
+
2160
2545
  buildAnimationCSS() {
2161
2546
  let css = '';
2162
2547
 
@@ -2371,6 +2756,139 @@ ${scripts}
2371
2756
  ` </div>`;
2372
2757
  }
2373
2758
 
2759
+ case 'Form': {
2760
+ const id = this.uid();
2761
+ const content = this.compileChildren(node.children || []);
2762
+ this.scripts.push(
2763
+ `document.getElementById('${id}').addEventListener('submit', function(e) {\n` +
2764
+ ` e.preventDefault();\n` +
2765
+ ` alert('Form submitted!');\n` +
2766
+ ` });`
2767
+ );
2768
+ const action = node.action ? ` action="${this.escapeHtml(node.action)}"` : '';
2769
+ return ` <form id="${id}"${action} class="clou-form">\n${content}\n </form>`;
2770
+ }
2771
+
2772
+ case 'Table': {
2773
+ const rows = (node.children || []).filter(c => c.type === 'Row' || c.type === 'TableRow');
2774
+ let html = ' <table class="clou-table">\n';
2775
+ for (const row of node.children || []) {
2776
+ if (row.type === 'Heading') {
2777
+ // Table heading row
2778
+ html += ' <thead><tr>';
2779
+ const cells = row.value.split(',').map(c => c.trim());
2780
+ for (const cell of cells) {
2781
+ html += `<th>${this.escapeHtml(this.interpolate(cell))}</th>`;
2782
+ }
2783
+ html += '</tr></thead>\n';
2784
+ } else if (row.children) {
2785
+ html += ' <tr>';
2786
+ for (const cell of row.children || []) {
2787
+ if (cell.type === 'Text') {
2788
+ html += `<td>${this.escapeHtml(this.interpolate(cell.value))}</td>`;
2789
+ } else {
2790
+ html += `<td>${this.compileNode(cell)}</td>`;
2791
+ }
2792
+ }
2793
+ html += '</tr>\n';
2794
+ }
2795
+ }
2796
+ html += ' </table>';
2797
+ return html;
2798
+ }
2799
+
2800
+ case 'Tabs': {
2801
+ this.hasTabs = true;
2802
+ const tabId = this.uid();
2803
+ const tabs = (node.children || []).filter(c => c.type === 'Tab');
2804
+ let buttonsHtml = ` <div class="clou-tabs" id="${tabId}">\n <div class="clou-tab-buttons">`;
2805
+ let panelsHtml = '';
2806
+
2807
+ tabs.forEach((tab, i) => {
2808
+ const active = i === 0 ? ' active' : '';
2809
+ const panelId = `${tabId}-panel-${i}`;
2810
+ buttonsHtml += `\n <button class="clou-tab-btn${active}" data-panel="${panelId}" onclick="document.querySelectorAll('#${tabId} .clou-tab-btn').forEach(b=>b.classList.remove('active'));this.classList.add('active');document.querySelectorAll('#${tabId} .clou-tab-panel').forEach(p=>p.classList.remove('active'));document.getElementById('${panelId}').classList.add('active');">${this.escapeHtml(this.interpolate(tab.name))}</button>`;
2811
+ const content = this.compileChildren(this.filterContent(tab.children || []));
2812
+ panelsHtml += `\n <div id="${panelId}" class="clou-tab-panel${active}">\n${content}\n </div>`;
2813
+ });
2814
+
2815
+ buttonsHtml += `\n </div>`;
2816
+ return `${buttonsHtml}${panelsHtml}\n </div>`;
2817
+ }
2818
+
2819
+ case 'Tab':
2820
+ // Handled by Tabs parent
2821
+ return '';
2822
+
2823
+ case 'Accordion': {
2824
+ this.hasAccordion = true;
2825
+ const panels = (node.children || []).filter(c => c.type === 'Panel');
2826
+ let html = ' <div class="clou-accordion">';
2827
+ for (const panel of panels) {
2828
+ const panelId = this.uid();
2829
+ const content = this.compileChildren(this.filterContent(panel.children || []));
2830
+ html += `\n <div class="clou-panel" id="${panelId}">`;
2831
+ html += `\n <div class="clou-panel-header" onclick="this.parentElement.classList.toggle('open')">${this.escapeHtml(this.interpolate(panel.name))}<span class="clou-panel-arrow">&#9660;</span></div>`;
2832
+ html += `\n <div class="clou-panel-body">\n${content}\n </div>`;
2833
+ html += `\n </div>`;
2834
+ }
2835
+ html += '\n </div>';
2836
+ return html;
2837
+ }
2838
+
2839
+ case 'Panel':
2840
+ // Handled by Accordion parent
2841
+ return '';
2842
+
2843
+ case 'Progress': {
2844
+ this.hasProgress = true;
2845
+ const pct = parseInt(node.value, 10) || 0;
2846
+ const label = node.label ? `\n <div class="clou-progress-label">${this.escapeHtml(this.interpolate(node.label))}</div>` : '';
2847
+ return ` <div class="clou-progress">${label}\n <div class="clou-progress-bar"><div class="clou-progress-fill" style="width: ${pct}%"></div></div>\n </div>`;
2848
+ }
2849
+
2850
+ case 'Dropdown': {
2851
+ const id = this.uid();
2852
+ const options = (node.children || []).filter(c => c.type === 'Option');
2853
+ let html = ` <div class="clou-dropdown">\n <label for="${id}">${this.escapeHtml(this.interpolate(node.label))}</label>\n <select id="${id}" class="clou-input">`;
2854
+ for (const opt of options) {
2855
+ html += `\n <option value="${this.escapeHtml(opt.value)}">${this.escapeHtml(this.interpolate(opt.value))}</option>`;
2856
+ }
2857
+ html += '\n </select>\n </div>';
2858
+ return html;
2859
+ }
2860
+
2861
+ case 'Option':
2862
+ // Handled by Dropdown parent
2863
+ return '';
2864
+
2865
+ case 'Textarea': {
2866
+ const id = this.uid();
2867
+ const style = this.extractInlineStyles(node.children || []);
2868
+ const styleAttr = style ? ` style="${style}"` : '';
2869
+ return ` <textarea id="${id}" placeholder="${this.escapeHtml(this.interpolate(node.placeholder))}" rows="4"${styleAttr}></textarea>`;
2870
+ }
2871
+
2872
+ case 'Checkbox': {
2873
+ const id = this.uid();
2874
+ return ` <label class="clou-checkbox"><input type="checkbox" id="${id}"> ${this.escapeHtml(this.interpolate(node.label))}</label>`;
2875
+ }
2876
+
2877
+ case 'Audio':
2878
+ return ` <audio src="${this.escapeHtml(node.src)}" controls class="clou-audio"></audio>`;
2879
+
2880
+ case 'Code':
2881
+ return ` <pre class="clou-code"><code>${this.escapeHtml(this.interpolate(node.value))}</code></pre>`;
2882
+
2883
+ case 'Slider': {
2884
+ this.hasSlider = true;
2885
+ const id = this.uid();
2886
+ return ` <div class="clou-slider">\n <label for="${id}">${this.escapeHtml(this.interpolate(node.label))}</label>\n <input type="range" id="${id}" min="${node.min}" max="${node.max}">\n </div>`;
2887
+ }
2888
+
2889
+ case 'Submit':
2890
+ return ` <button type="submit">${this.escapeHtml(this.interpolate(node.label))}</button>`;
2891
+
2374
2892
  case 'Icon':
2375
2893
  return ` <span class="clou-icon">${node.value}</span>`;
2376
2894