tjs-lang 0.2.8 → 0.3.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.
@@ -26,6 +26,14 @@ import { codeMirror, CodeMirror } from '../../editors/codemirror/component'
26
26
  import { tjs, type TJSTranspileOptions } from '../../src/lang'
27
27
  import { generateDocsMarkdown } from './docs-utils'
28
28
  import { extractImports, generateImportMap, resolveImports } from './imports'
29
+ import {
30
+ buildIframeDoc,
31
+ createIframeMessageHandler,
32
+ renderConsoleMessages,
33
+ renderTestResults,
34
+ formatExecTime,
35
+ sharedPlaygroundStyles,
36
+ } from './playground-shared'
29
37
  import { ModuleStore, type ValidationResult } from './module-store'
30
38
  import {
31
39
  buildAutocompleteContext,
@@ -438,42 +446,11 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
438
446
  }
439
447
 
440
448
  private renderConsole() {
441
- // Parse messages for line references and make them clickable
442
- // Patterns: "at line X", "line X:", "Line X", ":X:" (line:col)
443
- const linePattern =
444
- /(?:at line |line |Line )(\d+)(?:[:,]?\s*(?:column |col )?(\d+))?|:(\d+):(\d+)/g
445
-
446
- const html = this.consoleMessages
447
- .map((msg) => {
448
- // Escape HTML
449
- const escaped = msg
450
- .replace(/&/g, '&amp;')
451
- .replace(/</g, '&lt;')
452
- .replace(/>/g, '&gt;')
453
-
454
- // Replace line references with clickable spans
455
- return escaped.replace(linePattern, (match, l1, c1, l2, c2) => {
456
- const line = l1 || l2
457
- const col = c1 || c2 || '1'
458
- return `<span class="clickable-line" data-line="${line}" data-col="${col}">${match}</span>`
459
- })
460
- })
461
- .join('\n')
462
-
463
- this.parts.console.innerHTML = html
464
- this.parts.console.scrollTop = this.parts.console.scrollHeight
465
-
466
- // Add click handlers
467
- this.parts.console.querySelectorAll('.clickable-line').forEach((el) => {
468
- el.addEventListener('click', (e) => {
469
- const target = e.currentTarget as HTMLElement
470
- const line = parseInt(target.dataset.line || '0', 10)
471
- const col = parseInt(target.dataset.col || '1', 10)
472
- if (line > 0) {
473
- this.goToSourceLine(line, col)
474
- }
475
- })
476
- })
449
+ renderConsoleMessages(
450
+ this.consoleMessages,
451
+ this.parts.console,
452
+ (line, col) => this.goToSourceLine(line, col)
453
+ )
477
454
  }
478
455
 
479
456
  // Build flag toggle handlers
@@ -548,10 +525,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
548
525
  // Update test results and status bar with timing
549
526
  const tests = result.testResults || []
550
527
  const failed = tests.filter((t: any) => !t.passed).length
551
- const timeStr =
552
- this.lastTranspileTime < 1
553
- ? `${(this.lastTranspileTime * 1000).toFixed(0)}μs`
554
- : `${this.lastTranspileTime.toFixed(2)}ms`
528
+ const timeStr = formatExecTime(this.lastTranspileTime)
555
529
  if (failed > 0) {
556
530
  this.parts.statusBar.textContent = `Transpiled in ${timeStr} with ${failed} test failure${
557
531
  failed > 1 ? 's' : ''
@@ -619,75 +593,13 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
619
593
 
620
594
  private updateTestResults(result: any) {
621
595
  const tests = result.testResults
622
- if (!tests || tests.length === 0) {
623
- this.parts.testsOutput.textContent = 'No tests defined'
624
- this.updateTestsTabLabel(0, 0)
625
- this.parts.tjsEditor.clearMarkers()
626
- return
627
- }
628
-
629
- const passed = tests.filter((t: any) => t.passed).length
630
- const failed = tests.filter((t: any) => !t.passed).length
631
-
632
- // Update tab label with indicator
596
+ const { passed, failed } = renderTestResults(
597
+ tests,
598
+ this.parts.testsOutput,
599
+ this.parts.tjsEditor,
600
+ (line) => this.goToSourceLine(line)
601
+ )
633
602
  this.updateTestsTabLabel(passed, failed)
634
-
635
- // Set gutter markers for failed tests
636
- const failedTests = tests.filter((t: any) => !t.passed && t.line)
637
- if (failedTests.length > 0) {
638
- this.parts.tjsEditor.setMarkers(
639
- failedTests.map((t: any) => ({
640
- line: t.line,
641
- message: t.error || t.description,
642
- severity: 'error' as const,
643
- }))
644
- )
645
- } else {
646
- this.parts.tjsEditor.clearMarkers()
647
- }
648
-
649
- let html = `<div class="test-summary">`
650
- html += `<strong>${passed} passed</strong>`
651
- if (failed > 0) {
652
- html += `, <strong class="test-failed">${failed} failed</strong>`
653
- }
654
- html += `</div><ul class="test-list">`
655
-
656
- for (const test of tests) {
657
- const icon = test.passed ? '✓' : '✗'
658
- const cls = test.passed ? 'test-pass' : 'test-fail'
659
- const sigBadge = test.isSignatureTest
660
- ? ' <span class="sig-badge">signature</span>'
661
- : ''
662
- const clickable =
663
- !test.passed && test.line ? ' class="clickable-error"' : ''
664
- const dataLine = test.line ? ` data-line="${test.line}"` : ''
665
- html += `<li class="${cls}"${dataLine}>${icon} ${test.description}${sigBadge}`
666
- if (!test.passed && test.error) {
667
- html += `<div${clickable}${dataLine} class="test-error${
668
- test.line ? ' clickable-error' : ''
669
- }">${test.error}</div>`
670
- }
671
- html += `</li>`
672
- }
673
- html += `</ul>`
674
-
675
- this.parts.testsOutput.innerHTML = html
676
-
677
- // Add click handlers for clickable errors
678
- this.parts.testsOutput
679
- .querySelectorAll('.clickable-error')
680
- .forEach((el) => {
681
- el.addEventListener('click', (e) => {
682
- const line = parseInt(
683
- (e.currentTarget as HTMLElement).dataset.line || '0',
684
- 10
685
- )
686
- if (line > 0) {
687
- this.goToSourceLine(line)
688
- }
689
- })
690
- })
691
603
  }
692
604
 
693
605
  private updateTestsTabLabel(passed: number, failed: number) {
@@ -1052,6 +964,9 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
1052
964
  return
1053
965
  }
1054
966
 
967
+ // Show JS output immediately after successful transpilation
968
+ this.parts.outputTabs.value = 0 // JS is first tab (index 0)
969
+
1055
970
  this.parts.statusBar.textContent = 'Running...'
1056
971
 
1057
972
  try {
@@ -1111,137 +1026,33 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
1111
1026
  )
1112
1027
 
1113
1028
  // Create a complete HTML document for the iframe
1114
- const iframeDoc = `<!DOCTYPE html>
1115
- <html>
1116
- <head>
1117
- <style>${cssContent}</style>
1118
- ${importMapScript}
1119
- </head>
1120
- <body>
1121
- ${htmlContent}
1122
- <!-- TJS Runtime stub must be set up BEFORE imports execute -->
1123
- <script>
1124
- // Expose parent's run/runAgent/getIdToken in iframe for playground convenience
1125
- if (parent.run) window.run = parent.run.bind(parent);
1126
- if (parent.runAgent) window.runAgent = parent.runAgent.bind(parent);
1127
- if (parent.getIdToken) window.getIdToken = parent.getIdToken.bind(parent);
1128
-
1129
- // TJS runtime stub - must stay in sync with src/lang/runtime.ts
1130
- // TODO: Eliminate this once transpiler emits self-contained code
1131
- // See: src/lang/emitters/js.ts for the plan to inline runtime functions
1132
- globalThis.__tjs = {
1133
- version: '0.0.0',
1134
- pushStack: () => {},
1135
- popStack: () => {},
1136
- getStack: () => [],
1137
- typeError: (path, expected, value) => {
1138
- const actual = value === null ? 'null' : typeof value;
1139
- const err = new Error(\`Expected \${expected} for '\${path}', got \${actual}\`);
1140
- err.name = 'MonadicError';
1141
- err.path = path;
1142
- err.expected = expected;
1143
- err.actual = actual;
1144
- return err;
1145
- },
1146
- createRuntime: function() { return this; },
1147
- Is: (a, b) => {
1148
- if (a === b) return true;
1149
- if (a === null || b === null) return a === b;
1150
- if (typeof a !== typeof b) return false;
1151
- if (typeof a !== 'object') return false;
1152
- if (Array.isArray(a) && Array.isArray(b)) {
1153
- if (a.length !== b.length) return false;
1154
- return a.every((v, i) => globalThis.__tjs.Is(v, b[i]));
1155
- }
1156
- const keysA = Object.keys(a);
1157
- const keysB = Object.keys(b);
1158
- if (keysA.length !== keysB.length) return false;
1159
- return keysA.every(k => globalThis.__tjs.Is(a[k], b[k]));
1160
- },
1161
- IsNot: (a, b) => !globalThis.__tjs.Is(a, b),
1162
- };
1163
- </script>
1164
- <script type="module">
1165
- // Import statements must be at the top of the module
1166
- ${importStatements.join('\n ')}
1167
-
1168
- // Capture console.log
1169
- const _log = console.log;
1170
- console.log = (...args) => {
1171
- _log(...args);
1172
- parent.postMessage({ type: 'console', message: args.map(a => {
1173
- if (typeof a !== 'object' || a === null) return String(a);
1174
- try {
1175
- return JSON.stringify(a, null, 2);
1176
- } catch {
1177
- return String(a);
1178
- }
1179
- }).join(' ') }, '*');
1180
- };
1181
-
1182
- try {
1183
- // WASM blocks are pre-compiled and embedded in the transpiled code
1184
- // They auto-instantiate via the async IIFE at the top of the code
1185
-
1186
- const __execStart = performance.now();
1187
- ${codeWithoutImports}
1188
-
1189
- // Try to call the function if it exists and show result
1190
- const funcName = Object.keys(window).find(k => {
1191
- try { return typeof window[k] === 'function' && window[k].__tjs; }
1192
- catch { return false; }
1193
- });
1194
- if (funcName) {
1195
- const __callStart = performance.now();
1196
- const result = window[funcName]();
1197
- const __execTime = performance.now() - __callStart;
1198
- parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
1199
- if (result !== undefined) {
1200
- // If result is a DOM node, append it; otherwise log it
1201
- if (result instanceof Node) {
1202
- document.body.append(result);
1203
- parent.postMessage({ type: 'hasPreviewContent' }, '*');
1204
- } else {
1205
- console.log('Result:', result);
1206
- }
1207
- }
1208
- } else {
1209
- // No TJS function found, report total parse/exec time
1210
- const __execTime = performance.now() - __execStart;
1211
- parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
1212
- }
1213
- // Check if body has content after execution
1214
- if (document.body.children.length > 0) {
1215
- parent.postMessage({ type: 'hasPreviewContent' }, '*');
1216
- }
1217
- } catch (e) {
1218
- parent.postMessage({ type: 'error', message: e.message }, '*');
1219
- }
1220
- </script>
1221
- </body>
1222
- </html>`
1029
+ const iframeDoc = buildIframeDoc({
1030
+ cssContent,
1031
+ htmlContent,
1032
+ importMapScript,
1033
+ jsCode: codeWithoutImports,
1034
+ importStatements,
1035
+ parentBindings: true,
1036
+ autoCallTjsFunction: true,
1037
+ })
1223
1038
 
1224
1039
  // Listen for messages from iframe
1225
- const messageHandler = (event: MessageEvent) => {
1226
- if (event.data?.type === 'console') {
1227
- this.log(event.data.message)
1228
- } else if (event.data?.type === 'timing') {
1229
- // Update console header with execution time
1230
- const execTime = event.data.execTime
1231
- const execStr =
1232
- execTime < 1
1233
- ? `${(execTime * 1000).toFixed(0)}μs`
1234
- : `${execTime.toFixed(2)}ms`
1235
- this.parts.consoleHeader.textContent = `Console — executed in ${execStr}`
1236
- } else if (event.data?.type === 'hasPreviewContent') {
1237
- // Switch to Preview tab when content is added
1040
+ const messageHandler = createIframeMessageHandler({
1041
+ onConsole: (message) => this.log(message),
1042
+ onTiming: (execTime) => {
1043
+ this.parts.consoleHeader.textContent = `Console executed in ${formatExecTime(
1044
+ execTime
1045
+ )}`
1046
+ },
1047
+ onPreviewContent: () => {
1238
1048
  this.parts.outputTabs.value = 1 // Preview is second tab (index 1)
1239
- } else if (event.data?.type === 'error') {
1240
- this.log(`Error: ${event.data.message}`)
1049
+ },
1050
+ onError: (message) => {
1051
+ this.log(`Error: ${message}`)
1241
1052
  this.parts.statusBar.textContent = 'Runtime error'
1242
1053
  this.parts.statusBar.classList.add('error')
1243
- }
1244
- }
1054
+ },
1055
+ })
1245
1056
  window.addEventListener('message', messageHandler)
1246
1057
 
1247
1058
  // Set iframe content using blob URL instead of srcdoc
@@ -1339,16 +1150,9 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
1339
1150
  export const tjsPlayground = TJSPlayground.elementCreator({
1340
1151
  tag: 'tjs-playground',
1341
1152
  styleSpec: {
1342
- ':host': {
1343
- display: 'flex',
1344
- flexDirection: 'column',
1345
- height: '100%',
1346
- flex: '1 1 auto',
1347
- background: 'var(--background, #fff)',
1348
- color: 'var(--text-color, #1f2937)',
1349
- fontFamily: 'system-ui, sans-serif',
1350
- },
1153
+ ...sharedPlaygroundStyles,
1351
1154
 
1155
+ // TJS-specific: toolbar
1352
1156
  ':host .tjs-toolbar': {
1353
1157
  display: 'flex',
1354
1158
  alignItems: 'center',
@@ -1358,61 +1162,7 @@ export const tjsPlayground = TJSPlayground.elementCreator({
1358
1162
  borderBottom: '1px solid var(--code-border, #e5e7eb)',
1359
1163
  },
1360
1164
 
1361
- ':host .run-btn': {
1362
- display: 'flex',
1363
- alignItems: 'center',
1364
- gap: '4px',
1365
- padding: '6px 12px',
1366
- background: 'var(--brand-color, #3d4a6b)',
1367
- color: 'var(--brand-text-color, white)',
1368
- border: 'none',
1369
- borderRadius: '6px',
1370
- cursor: 'pointer',
1371
- fontWeight: '500',
1372
- fontSize: '14px',
1373
- },
1374
-
1375
- ':host .run-btn:hover:not(:disabled)': {
1376
- filter: 'brightness(1.1)',
1377
- },
1378
-
1379
- ':host .run-btn:disabled': {
1380
- opacity: '0.6',
1381
- cursor: 'not-allowed',
1382
- },
1383
-
1384
- ':host .toolbar-separator': {
1385
- width: '1px',
1386
- height: '20px',
1387
- background: 'var(--code-border, #d1d5db)',
1388
- },
1389
-
1390
- ':host .build-flags': {
1391
- display: 'flex',
1392
- alignItems: 'center',
1393
- gap: '12px',
1394
- },
1395
-
1396
- ':host .flag-label': {
1397
- display: 'flex',
1398
- alignItems: 'center',
1399
- gap: '4px',
1400
- fontSize: '13px',
1401
- color: 'var(--text-color, #6b7280)',
1402
- cursor: 'pointer',
1403
- userSelect: 'none',
1404
- },
1405
-
1406
- ':host .flag-label:hover': {
1407
- color: 'var(--text-color, #374151)',
1408
- },
1409
-
1410
- ':host .flag-label input[type="checkbox"]': {
1411
- margin: '0',
1412
- cursor: 'pointer',
1413
- accentColor: 'var(--brand-color, #3d4a6b)',
1414
- },
1415
-
1165
+ // TJS-specific: module name input
1416
1166
  ':host .module-name-input': {
1417
1167
  padding: '6px 10px',
1418
1168
  border: '1px solid var(--code-border, #d1d5db)',
@@ -1435,6 +1185,7 @@ export const tjsPlayground = TJSPlayground.elementCreator({
1435
1185
  opacity: '0.6',
1436
1186
  },
1437
1187
 
1188
+ // TJS-specific: save button
1438
1189
  ':host .save-btn': {
1439
1190
  display: 'flex',
1440
1191
  alignItems: 'center',
@@ -1455,46 +1206,7 @@ export const tjsPlayground = TJSPlayground.elementCreator({
1455
1206
  borderColor: 'var(--brand-color, #3d4a6b)',
1456
1207
  },
1457
1208
 
1458
- ':host .revert-btn': {
1459
- display: 'flex',
1460
- alignItems: 'center',
1461
- gap: '4px',
1462
- padding: '6px 12px',
1463
- background: 'var(--code-background, #e5e7eb)',
1464
- color: 'var(--text-color, #374151)',
1465
- border: '1px solid var(--code-border, #d1d5db)',
1466
- borderRadius: '6px',
1467
- cursor: 'pointer',
1468
- fontWeight: '500',
1469
- fontSize: '14px',
1470
- transition: 'opacity 0.2s',
1471
- },
1472
-
1473
- ':host .revert-btn:hover:not(:disabled)': {
1474
- background: '#fef3c7',
1475
- borderColor: '#f59e0b',
1476
- color: '#92400e',
1477
- },
1478
-
1479
- ':host .revert-btn:disabled': {
1480
- cursor: 'default',
1481
- },
1482
-
1483
- ':host .elastic': {
1484
- flex: '1',
1485
- },
1486
-
1487
- ':host .status-bar': {
1488
- fontSize: '13px',
1489
- color: 'var(--text-color, #6b7280)',
1490
- opacity: '0.7',
1491
- },
1492
-
1493
- ':host .status-bar.error': {
1494
- color: '#dc2626',
1495
- opacity: '1',
1496
- },
1497
-
1209
+ // TJS-specific: layout
1498
1210
  ':host .tjs-main': {
1499
1211
  display: 'flex',
1500
1212
  flex: '1 1 auto',
@@ -1512,31 +1224,7 @@ export const tjsPlayground = TJSPlayground.elementCreator({
1512
1224
  overflow: 'hidden',
1513
1225
  },
1514
1226
 
1515
- // Tab content panels need explicit background for dark mode
1516
- ':host tosi-tabs > [name]': {
1517
- background: 'var(--background, #fff)',
1518
- color: 'var(--text-color, #1f2937)',
1519
- },
1520
-
1521
- // Editor wrapper - contains the shadow DOM code-mirror component
1522
- ':host .editor-wrapper': {
1523
- flex: '1 1 auto',
1524
- height: '100%',
1525
- minHeight: '300px',
1526
- position: 'relative',
1527
- overflow: 'hidden',
1528
- },
1529
-
1530
- // code-mirror is shadow DOM, so we just size it - internal styles are handled by the component
1531
- ':host .editor-wrapper code-mirror': {
1532
- display: 'block',
1533
- position: 'absolute',
1534
- top: '0',
1535
- left: '0',
1536
- right: '0',
1537
- bottom: '0',
1538
- },
1539
-
1227
+ // TJS-specific: JS output panel
1540
1228
  ':host .js-output': {
1541
1229
  margin: '0',
1542
1230
  padding: '12px',
@@ -1549,176 +1237,12 @@ export const tjsPlayground = TJSPlayground.elementCreator({
1549
1237
  whiteSpace: 'pre-wrap',
1550
1238
  },
1551
1239
 
1552
- ':host .preview-frame': {
1553
- width: '100%',
1554
- height: '100%',
1555
- border: 'none',
1556
- background: 'var(--background, #fff)',
1557
- },
1558
-
1559
- ':host .docs-output': {
1560
- display: 'block',
1561
- padding: '12px 16px',
1562
- fontSize: '14px',
1563
- fontFamily: 'system-ui, sans-serif',
1564
- color: 'var(--text-color, inherit)',
1565
- background: 'var(--background, #fff)',
1566
- height: '100%',
1567
- overflow: 'auto',
1568
- },
1569
-
1570
- ':host .docs-output h2': {
1571
- fontSize: '1.25em',
1572
- marginTop: '0',
1573
- marginBottom: '0.5em',
1574
- color: 'var(--text-color, #1f2937)',
1575
- },
1576
-
1577
- ':host .docs-output pre': {
1578
- background: 'var(--code-background, #f3f4f6)',
1579
- padding: '8px 12px',
1580
- borderRadius: '6px',
1581
- overflow: 'auto',
1582
- fontSize: '13px',
1583
- },
1584
-
1585
- ':host .docs-output code': {
1586
- fontFamily: 'ui-monospace, monospace',
1587
- fontSize: '0.9em',
1588
- },
1589
-
1590
- ':host .docs-output p': {
1591
- margin: '0.75em 0',
1592
- lineHeight: '1.5',
1593
- },
1594
-
1595
- ':host .docs-output h3': {
1596
- fontSize: '1em',
1597
- marginTop: '1em',
1598
- marginBottom: '0.5em',
1599
- },
1600
-
1601
- ':host .docs-output ul': {
1602
- paddingLeft: '1.5em',
1603
- margin: '0.5em 0',
1604
- },
1605
-
1606
- ':host .docs-output li': {
1607
- marginBottom: '0.25em',
1608
- },
1609
-
1610
- ':host .docs-output hr': {
1611
- border: 'none',
1612
- borderTop: '1px solid var(--code-border, #e5e7eb)',
1613
- margin: '1.5em 0',
1614
- },
1615
-
1616
- ':host .tests-output': {
1617
- padding: '12px',
1618
- fontSize: '14px',
1619
- fontFamily: 'system-ui, sans-serif',
1620
- color: 'var(--text-color, inherit)',
1621
- background: 'var(--background, #fff)',
1622
- height: '100%',
1623
- overflow: 'auto',
1624
- },
1625
-
1626
- ':host .test-summary': {
1627
- marginBottom: '12px',
1628
- paddingBottom: '8px',
1629
- borderBottom: '1px solid var(--code-border, #e5e7eb)',
1630
- },
1631
-
1632
- ':host .test-failed': {
1633
- color: '#dc2626',
1634
- },
1635
-
1636
- ':host .test-list': {
1637
- listStyle: 'none',
1638
- padding: 0,
1639
- margin: 0,
1640
- },
1641
-
1642
- ':host .test-list li': {
1643
- padding: '4px 0',
1644
- },
1645
-
1646
- ':host .test-pass': {
1647
- color: '#16a34a',
1648
- },
1649
-
1650
- ':host .test-fail': {
1651
- color: '#dc2626',
1652
- },
1653
-
1654
- ':host .test-error': {
1655
- marginLeft: '20px',
1656
- marginTop: '4px',
1657
- padding: '8px',
1658
- background: 'rgba(220, 38, 38, 0.1)',
1659
- borderRadius: '4px',
1660
- fontSize: '13px',
1661
- fontFamily: 'var(--font-mono, monospace)',
1662
- },
1663
-
1664
- ':host .clickable-error': {
1665
- cursor: 'pointer',
1666
- textDecoration: 'underline',
1667
- textDecorationStyle: 'dotted',
1668
- },
1669
-
1670
- ':host .clickable-error:hover': {
1671
- background: 'rgba(220, 38, 38, 0.2)',
1672
- },
1673
-
1674
- ':host .sig-badge': {
1675
- fontSize: '11px',
1676
- padding: '2px 6px',
1677
- marginLeft: '8px',
1678
- background: 'rgba(99, 102, 241, 0.1)',
1679
- color: '#6366f1',
1680
- borderRadius: '4px',
1681
- },
1682
-
1240
+ // TJS-specific: console container class name
1683
1241
  ':host .tjs-console': {
1684
1242
  height: '120px',
1685
1243
  borderTop: '1px solid var(--code-border, #e5e7eb)',
1686
1244
  display: 'flex',
1687
1245
  flexDirection: 'column',
1688
1246
  },
1689
-
1690
- ':host .console-header': {
1691
- padding: '4px 12px',
1692
- background: 'var(--code-background, #f3f4f6)',
1693
- fontSize: '12px',
1694
- fontWeight: '500',
1695
- color: 'var(--text-color, #6b7280)',
1696
- opacity: '0.7',
1697
- borderBottom: '1px solid var(--code-border, #e5e7eb)',
1698
- },
1699
-
1700
- ':host .console-output': {
1701
- flex: '1',
1702
- margin: '0',
1703
- padding: '8px 12px',
1704
- background: 'var(--code-background, #f3f4f6)',
1705
- color: 'var(--text-color, #1f2937)',
1706
- fontSize: '12px',
1707
- fontFamily: 'ui-monospace, monospace',
1708
- overflow: 'auto',
1709
- whiteSpace: 'pre-wrap',
1710
- },
1711
-
1712
- ':host .clickable-line': {
1713
- cursor: 'pointer',
1714
- color: '#2563eb',
1715
- textDecoration: 'underline',
1716
- textDecorationStyle: 'dotted',
1717
- },
1718
-
1719
- ':host .clickable-line:hover': {
1720
- color: '#1d4ed8',
1721
- background: 'rgba(37, 99, 235, 0.1)',
1722
- },
1723
1247
  },
1724
1248
  }) as ElementCreator<TJSPlayground>
@@ -382,25 +382,26 @@ main()
382
382
  description: 'Classes are supported with metadata',
383
383
  group: 'advanced',
384
384
  code: `// Classes with typed methods
385
+ // Note: return types omitted on methods — TJS infers them
385
386
 
386
387
  class Calculator {
387
388
  private value: number = 0
388
389
 
389
- add(n: number): Calculator {
390
+ add(n: number) {
390
391
  this.value += n
391
392
  return this
392
393
  }
393
394
 
394
- multiply(n: number): Calculator {
395
+ multiply(n: number) {
395
396
  this.value *= n
396
397
  return this
397
398
  }
398
399
 
399
- getResult(): number {
400
+ getResult() {
400
401
  return this.value
401
402
  }
402
403
 
403
- reset(): void {
404
+ reset() {
404
405
  this.value = 0
405
406
  }
406
407
  }