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.
- package/ai/clou-ai-prompt.md +74 -9
- package/bin/clou.js +65 -8
- package/examples/hello.html +80 -2
- package/examples/themes-demo.clou +1 -1
- package/examples/themes-demo.html +76 -48
- package/package.json +1 -1
- package/playground/clou-browser.js +523 -5
- package/playground/index.html +72 -0
- package/src/errors.js +159 -29
- package/src/lexer.js +9 -0
- package/src/terminal-parser.js +9 -0
- package/src/terminal-runtime.js +15 -0
|
@@ -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.
|
|
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">▼</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
|
|