tjs-lang 0.2.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.
Files changed (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
@@ -0,0 +1,1605 @@
1
+ /**
2
+ * TJS Playground - Interactive TJS editor and runner
3
+ *
4
+ * A light-DOM web component for editing and running TJS code.
5
+ * Features:
6
+ * - CodeMirror editor with TJS syntax highlighting
7
+ * - Tabbed output: JS output, Preview, Docs, Tests
8
+ * - CSS/HTML editing for preview customization
9
+ * - Console output panel
10
+ */
11
+
12
+ import { Component, ElementCreator, PartsMap, elements, vars } from 'tosijs'
13
+ import {
14
+ tabSelector,
15
+ TabSelector,
16
+ icons,
17
+ markdownViewer,
18
+ MarkdownViewer,
19
+ } from 'tosijs-ui'
20
+ import { codeMirror, CodeMirror } from '../../editors/codemirror/component'
21
+ import { tjs, type TJSTranspileOptions } from '../../src/lang'
22
+ import { generateDocs } from '../../src/lang/docs'
23
+ import { extractImports, generateImportMap, resolveImports } from './imports'
24
+ import { ModuleStore, type ValidationResult } from './module-store'
25
+
26
+ const { div, button, span, pre, style, input, template } = elements
27
+
28
+ /**
29
+ * Convert TypeDescriptor to readable string
30
+ * Handles both old format (type: 'string') and new format (type: { kind: 'string', ... })
31
+ */
32
+ function typeToString(type: any): string {
33
+ if (!type) return 'unknown'
34
+ // Old format: type was just a string like 'string', 'number', etc.
35
+ if (typeof type === 'string') return type
36
+
37
+ // New format: type is an object with kind, shape, items, members
38
+ switch (type.kind) {
39
+ case 'string':
40
+ return type.nullable ? 'string | null' : 'string'
41
+ case 'number':
42
+ return type.nullable ? 'number | null' : 'number'
43
+ case 'boolean':
44
+ return type.nullable ? 'boolean | null' : 'boolean'
45
+ case 'null':
46
+ return 'null'
47
+ case 'undefined':
48
+ return 'undefined'
49
+ case 'any':
50
+ return 'any'
51
+ case 'array': {
52
+ const itemType = type.items ? typeToString(type.items) : 'any'
53
+ return type.nullable ? `${itemType}[] | null` : `${itemType}[]`
54
+ }
55
+ case 'object': {
56
+ if (!type.shape) return type.nullable ? 'object | null' : 'object'
57
+ const props = Object.entries(type.shape)
58
+ .map(([k, v]) => `${k}: ${typeToString(v)}`)
59
+ .join(', ')
60
+ const objStr = `{ ${props} }`
61
+ return type.nullable ? `${objStr} | null` : objStr
62
+ }
63
+ case 'union':
64
+ return type.members?.map(typeToString).join(' | ') || 'unknown'
65
+ default:
66
+ return type.kind || 'unknown'
67
+ }
68
+ }
69
+
70
+ // Example TJS code
71
+ const DEFAULT_TJS = `// TJS Example - Type annotations via examples
72
+ function greet(name: 'World') -> '' {
73
+ return \`Hello, \${name}!\`
74
+ }
75
+
76
+ // Call it
77
+ greet('TJS')`
78
+
79
+ const DEFAULT_HTML = `<div class="preview-content">
80
+ <h2>Preview</h2>
81
+ <div id="output"></div>
82
+ </div>`
83
+
84
+ const DEFAULT_CSS = `.preview-content {
85
+ padding: 1rem;
86
+ font-family: system-ui, sans-serif;
87
+ }
88
+
89
+ h2 {
90
+ color: #3d4a6b;
91
+ margin-top: 0;
92
+ }
93
+
94
+ #output {
95
+ padding: 0.5rem;
96
+ background: #f5f5f5;
97
+ border-radius: 4px;
98
+ }`
99
+
100
+ interface TJSPlaygroundParts extends PartsMap {
101
+ tjsEditor: CodeMirror
102
+ htmlEditor: CodeMirror
103
+ cssEditor: CodeMirror
104
+ inputTabs: TabSelector
105
+ outputTabs: TabSelector
106
+ jsOutput: HTMLElement
107
+ previewFrame: HTMLIFrameElement
108
+ docsOutput: MarkdownViewer
109
+ testsOutput: HTMLElement
110
+ consoleHeader: HTMLElement
111
+ console: HTMLElement
112
+ runBtn: HTMLButtonElement
113
+ revertBtn: HTMLButtonElement
114
+ saveBtn: HTMLButtonElement
115
+ moduleNameInput: HTMLInputElement
116
+ statusBar: HTMLElement
117
+ // Build flags
118
+ testsToggle: HTMLInputElement
119
+ debugToggle: HTMLInputElement
120
+ safetyToggle: HTMLInputElement
121
+ }
122
+
123
+ export class TJSPlayground extends Component<TJSPlaygroundParts> {
124
+ private lastTranspileResult: any = null
125
+ private consoleMessages: string[] = []
126
+ private functionMetadata: Record<string, any> = {}
127
+
128
+ // Editor state persistence
129
+ private currentExampleName: string | null = null
130
+ private originalCode: string = DEFAULT_TJS
131
+ private editorCache: Map<string, string> = new Map()
132
+
133
+ // Build flags state
134
+ private buildFlags = {
135
+ tests: true, // Run tests at transpile time
136
+ debug: false, // Debug mode (call stack tracking)
137
+ safe: true, // Safe mode (validates inputs)
138
+ }
139
+
140
+ // Transpilation sequence number to handle race conditions
141
+ private transpileSeq = 0
142
+
143
+ /**
144
+ * Get metadata for autocomplete - returns all discovered functions
145
+ */
146
+ private getMetadataForAutocomplete = (): Record<string, any> | undefined => {
147
+ if (Object.keys(this.functionMetadata).length === 0) {
148
+ return undefined
149
+ }
150
+ return this.functionMetadata
151
+ }
152
+
153
+ content = () => [
154
+ // Toolbar
155
+ div(
156
+ { class: 'tjs-toolbar' },
157
+ button(
158
+ { part: 'runBtn', class: 'run-btn', onClick: this.run },
159
+ icons.play({ size: 16 }),
160
+ 'Run'
161
+ ),
162
+ span({ class: 'toolbar-separator' }),
163
+ // Build flags
164
+ div(
165
+ { class: 'build-flags' },
166
+ elements.label(
167
+ { class: 'flag-label', title: 'Run tests at transpile time' },
168
+ input({
169
+ part: 'testsToggle',
170
+ type: 'checkbox',
171
+ checked: true,
172
+ onChange: this.toggleTests,
173
+ }),
174
+ 'Tests'
175
+ ),
176
+ elements.label(
177
+ { class: 'flag-label', title: 'Debug mode (call stack tracking)' },
178
+ input({
179
+ part: 'debugToggle',
180
+ type: 'checkbox',
181
+ onChange: this.toggleDebug,
182
+ }),
183
+ 'Debug'
184
+ ),
185
+ elements.label(
186
+ { class: 'flag-label', title: 'Safe mode (validates inputs)' },
187
+ input({
188
+ part: 'safetyToggle',
189
+ type: 'checkbox',
190
+ checked: true,
191
+ onChange: this.toggleSafety,
192
+ }),
193
+ 'Safe'
194
+ )
195
+ ),
196
+ span({ class: 'toolbar-separator' }),
197
+ button(
198
+ {
199
+ part: 'revertBtn',
200
+ class: 'revert-btn',
201
+ onClick: this.revertToOriginal,
202
+ title: 'Revert to original example code',
203
+ },
204
+ icons.cornerUpLeft({ size: 16 }),
205
+ 'Revert'
206
+ ),
207
+ span({ class: 'toolbar-separator' }),
208
+ input({
209
+ part: 'moduleNameInput',
210
+ class: 'module-name-input',
211
+ type: 'text',
212
+ placeholder: 'module-name',
213
+ title: 'Module name for saving/importing',
214
+ }),
215
+ button(
216
+ { part: 'saveBtn', class: 'save-btn', onClick: this.saveModule },
217
+ icons.save({ size: 16 }),
218
+ 'Save'
219
+ ),
220
+ span({ class: 'elastic' }),
221
+ span({ part: 'statusBar', class: 'status-bar' }, 'Ready')
222
+ ),
223
+
224
+ // Main area - split into input (left) and output (right)
225
+ div(
226
+ { class: 'tjs-main' },
227
+
228
+ // Input side - TJS, HTML, CSS editors
229
+ div(
230
+ { class: 'tjs-input' },
231
+ tabSelector(
232
+ { part: 'inputTabs' },
233
+ div(
234
+ { name: 'TJS', class: 'editor-wrapper' },
235
+ codeMirror({
236
+ part: 'tjsEditor',
237
+ mode: 'tjs',
238
+ })
239
+ ),
240
+ div(
241
+ { name: 'HTML', class: 'editor-wrapper' },
242
+ codeMirror({
243
+ part: 'htmlEditor',
244
+ mode: 'html',
245
+ })
246
+ ),
247
+ div(
248
+ { name: 'CSS', class: 'editor-wrapper' },
249
+ codeMirror({
250
+ part: 'cssEditor',
251
+ mode: 'css',
252
+ })
253
+ )
254
+ )
255
+ ),
256
+
257
+ // Output side - JS, Preview, Docs, Tests
258
+ div(
259
+ { class: 'tjs-output' },
260
+ tabSelector(
261
+ { part: 'outputTabs' },
262
+ div(
263
+ { name: 'JS' },
264
+ pre(
265
+ { part: 'jsOutput', class: 'js-output' },
266
+ '// Transpiled JavaScript will appear here'
267
+ )
268
+ ),
269
+ div(
270
+ { name: 'Preview' },
271
+ div(
272
+ { class: 'preview-container' },
273
+ // Using an iframe for isolation
274
+ elements.iframe({
275
+ part: 'previewFrame',
276
+ class: 'preview-frame',
277
+ sandbox: 'allow-scripts allow-same-origin',
278
+ })
279
+ )
280
+ ),
281
+ markdownViewer({
282
+ name: 'Docs',
283
+ part: 'docsOutput',
284
+ class: 'docs-output',
285
+ value: '*Documentation will appear here*',
286
+ }),
287
+ div(
288
+ { name: 'Tests' },
289
+ template(
290
+ { role: 'tab' },
291
+ span({
292
+ class: 'test-indicator',
293
+ style: {
294
+ display: 'inline-block',
295
+ width: '8px',
296
+ height: '8px',
297
+ borderRadius: '50%',
298
+ marginRight: '6px',
299
+ background: vars.testIndicatorColor,
300
+ },
301
+ }),
302
+ 'Tests'
303
+ ),
304
+ div(
305
+ { part: 'testsOutput', class: 'tests-output' },
306
+ 'Test results will appear here'
307
+ )
308
+ )
309
+ )
310
+ )
311
+ ),
312
+
313
+ // Console panel at bottom
314
+ div(
315
+ { class: 'tjs-console' },
316
+ div({ part: 'consoleHeader', class: 'console-header' }, 'Console'),
317
+ pre({ part: 'console', class: 'console-output' })
318
+ ),
319
+ ]
320
+
321
+ connectedCallback(): void {
322
+ super.connectedCallback()
323
+
324
+ // Set default content
325
+ setTimeout(() => {
326
+ this.parts.tjsEditor.value = DEFAULT_TJS
327
+ this.parts.htmlEditor.value = DEFAULT_HTML
328
+ this.parts.cssEditor.value = DEFAULT_CSS
329
+
330
+ // Wire up autocomplete to get metadata from transpiler
331
+ this.parts.tjsEditor.autocomplete = {
332
+ getMetadata: this.getMetadataForAutocomplete,
333
+ }
334
+
335
+ // Auto-transpile on load
336
+ this.transpile()
337
+ }, 0)
338
+
339
+ // Listen for changes (debounced to avoid excessive transpilation)
340
+ let debounceTimer: ReturnType<typeof setTimeout>
341
+ this.parts.tjsEditor.addEventListener('change', () => {
342
+ clearTimeout(debounceTimer)
343
+ debounceTimer = setTimeout(() => {
344
+ this.transpile()
345
+ this.updateRevertButton()
346
+ }, 300)
347
+ })
348
+ }
349
+
350
+ log = (message: string) => {
351
+ this.consoleMessages.push(message)
352
+ this.renderConsole()
353
+ }
354
+
355
+ clearConsole = () => {
356
+ this.consoleMessages = []
357
+ this.parts.console.innerHTML = ''
358
+ this.parts.consoleHeader.textContent = 'Console'
359
+ }
360
+
361
+ private renderConsole() {
362
+ // Parse messages for line references and make them clickable
363
+ // Patterns: "at line X", "line X:", "Line X", ":X:" (line:col)
364
+ const linePattern =
365
+ /(?:at line |line |Line )(\d+)(?:[:,]?\s*(?:column |col )?(\d+))?|:(\d+):(\d+)/g
366
+
367
+ const html = this.consoleMessages
368
+ .map((msg) => {
369
+ // Escape HTML
370
+ const escaped = msg
371
+ .replace(/&/g, '&amp;')
372
+ .replace(/</g, '&lt;')
373
+ .replace(/>/g, '&gt;')
374
+
375
+ // Replace line references with clickable spans
376
+ return escaped.replace(linePattern, (match, l1, c1, l2, c2) => {
377
+ const line = l1 || l2
378
+ const col = c1 || c2 || '1'
379
+ return `<span class="clickable-line" data-line="${line}" data-col="${col}">${match}</span>`
380
+ })
381
+ })
382
+ .join('\n')
383
+
384
+ this.parts.console.innerHTML = html
385
+ this.parts.console.scrollTop = this.parts.console.scrollHeight
386
+
387
+ // Add click handlers
388
+ this.parts.console.querySelectorAll('.clickable-line').forEach((el) => {
389
+ el.addEventListener('click', (e) => {
390
+ const target = e.currentTarget as HTMLElement
391
+ const line = parseInt(target.dataset.line || '0', 10)
392
+ const col = parseInt(target.dataset.col || '1', 10)
393
+ if (line > 0) {
394
+ this.goToSourceLine(line, col)
395
+ }
396
+ })
397
+ })
398
+ }
399
+
400
+ // Build flag toggle handlers
401
+ toggleTests = () => {
402
+ this.buildFlags.tests = this.parts.testsToggle.checked
403
+ this.transpile()
404
+ }
405
+
406
+ toggleDebug = () => {
407
+ this.buildFlags.debug = this.parts.debugToggle.checked
408
+ this.transpile()
409
+ }
410
+
411
+ toggleSafety = () => {
412
+ this.buildFlags.safe = this.parts.safetyToggle.checked
413
+ this.transpile()
414
+ }
415
+
416
+ lastTranspileTime = 0
417
+
418
+ transpile = () => {
419
+ // Kick off async transpilation
420
+ this.transpileAsync()
421
+ }
422
+
423
+ private transpileAsync = async () => {
424
+ // Increment sequence number to track this transpilation
425
+ const mySeq = ++this.transpileSeq
426
+
427
+ let source = this.parts.tjsEditor.value
428
+
429
+ // Extract function metadata for autocomplete (even if transpile fails)
430
+ this.extractFunctionMetadata(source)
431
+
432
+ // Inject safety directive if unsafe mode is enabled
433
+ // This prepends "safety none" to skip all validation
434
+ if (!this.buildFlags.safe) {
435
+ source = 'safety none\n' + source
436
+ }
437
+
438
+ try {
439
+ // Resolve local imports before transpilation (for test execution)
440
+ const resolvedImports = await this.resolveImportsForTests(source)
441
+
442
+ // Check if a newer transpilation has started - if so, abandon this one
443
+ if (mySeq !== this.transpileSeq) {
444
+ return
445
+ }
446
+
447
+ // Time the transpilation
448
+ const startTime = performance.now()
449
+ // Build transpiler options from flags
450
+ const options: TJSTranspileOptions = {
451
+ runTests: this.buildFlags.tests ? 'report' : false,
452
+ debug: this.buildFlags.debug,
453
+ resolvedImports,
454
+ }
455
+ const result = tjs(source, options)
456
+ this.lastTranspileTime = performance.now() - startTime
457
+
458
+ this.lastTranspileResult = result
459
+ this.parts.jsOutput.textContent = result.code
460
+
461
+ // Update docs
462
+ this.updateDocs(result)
463
+
464
+ // Update test results and status bar with timing
465
+ const tests = result.testResults || []
466
+ const failed = tests.filter((t: any) => !t.passed).length
467
+ const timeStr =
468
+ this.lastTranspileTime < 1
469
+ ? `${(this.lastTranspileTime * 1000).toFixed(0)}μs`
470
+ : `${this.lastTranspileTime.toFixed(2)}ms`
471
+ if (failed > 0) {
472
+ this.parts.statusBar.textContent = `Transpiled in ${timeStr} with ${failed} test failure${
473
+ failed > 1 ? 's' : ''
474
+ }`
475
+ this.parts.statusBar.classList.add('error')
476
+ } else {
477
+ this.parts.statusBar.textContent = `Transpiled in ${timeStr}`
478
+ this.parts.statusBar.classList.remove('error')
479
+ }
480
+
481
+ this.updateTestResults(result)
482
+
483
+ // If we got metadata from transpiler, use it (more accurate)
484
+ if (result.metadata?.name && typeof result.metadata.name === 'string') {
485
+ this.functionMetadata[result.metadata.name] = result.metadata
486
+ }
487
+ } catch (e: any) {
488
+ // Format error with location info if available
489
+ const errorInfo = this.formatTranspileError(e, source)
490
+ this.parts.jsOutput.textContent = errorInfo.detailed
491
+ this.parts.statusBar.textContent = errorInfo.short
492
+ this.parts.statusBar.classList.add('error')
493
+ this.lastTranspileResult = null
494
+ // Clear test results on error
495
+ this.parts.testsOutput.textContent = 'Transpilation failed - no tests run'
496
+
497
+ // Set error marker in gutter
498
+ if (e.line) {
499
+ this.parts.tjsEditor.setMarkers([
500
+ { line: e.line, message: e.message || 'Transpilation error' },
501
+ ])
502
+ } else {
503
+ this.parts.tjsEditor.clearMarkers()
504
+ }
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Resolve local imports for test execution
510
+ * Returns a map of import specifier -> compiled code
511
+ */
512
+ private resolveImportsForTests = async (
513
+ source: string
514
+ ): Promise<Record<string, string>> => {
515
+ const imports = extractImports(source)
516
+ if (imports.length === 0) {
517
+ return {}
518
+ }
519
+
520
+ const resolvedImports: Record<string, string> = {}
521
+ const store = await ModuleStore.open()
522
+
523
+ for (const specifier of imports) {
524
+ // Only resolve local modules (not CDN packages)
525
+ if (await store.exists(specifier)) {
526
+ const compiled = await store.getCompiled(specifier)
527
+ if (compiled) {
528
+ resolvedImports[specifier] = compiled
529
+ }
530
+ }
531
+ }
532
+
533
+ return resolvedImports
534
+ }
535
+
536
+ private updateTestResults(result: any) {
537
+ const tests = result.testResults
538
+ if (!tests || tests.length === 0) {
539
+ this.parts.testsOutput.textContent = 'No tests defined'
540
+ this.updateTestsTabLabel(0, 0)
541
+ this.parts.tjsEditor.clearMarkers()
542
+ return
543
+ }
544
+
545
+ const passed = tests.filter((t: any) => t.passed).length
546
+ const failed = tests.filter((t: any) => !t.passed).length
547
+
548
+ // Update tab label with indicator
549
+ this.updateTestsTabLabel(passed, failed)
550
+
551
+ // Set gutter markers for failed tests
552
+ const failedTests = tests.filter((t: any) => !t.passed && t.line)
553
+ if (failedTests.length > 0) {
554
+ this.parts.tjsEditor.setMarkers(
555
+ failedTests.map((t: any) => ({
556
+ line: t.line,
557
+ message: t.error || t.description,
558
+ severity: 'error' as const,
559
+ }))
560
+ )
561
+ } else {
562
+ this.parts.tjsEditor.clearMarkers()
563
+ }
564
+
565
+ let html = `<div class="test-summary">`
566
+ html += `<strong>${passed} passed</strong>`
567
+ if (failed > 0) {
568
+ html += `, <strong class="test-failed">${failed} failed</strong>`
569
+ }
570
+ html += `</div><ul class="test-list">`
571
+
572
+ for (const test of tests) {
573
+ const icon = test.passed ? '✓' : '✗'
574
+ const cls = test.passed ? 'test-pass' : 'test-fail'
575
+ const sigBadge = test.isSignatureTest
576
+ ? ' <span class="sig-badge">signature</span>'
577
+ : ''
578
+ const clickable =
579
+ !test.passed && test.line ? ' class="clickable-error"' : ''
580
+ const dataLine = test.line ? ` data-line="${test.line}"` : ''
581
+ html += `<li class="${cls}"${dataLine}>${icon} ${test.description}${sigBadge}`
582
+ if (!test.passed && test.error) {
583
+ html += `<div${clickable}${dataLine} class="test-error${
584
+ test.line ? ' clickable-error' : ''
585
+ }">${test.error}</div>`
586
+ }
587
+ html += `</li>`
588
+ }
589
+ html += `</ul>`
590
+
591
+ this.parts.testsOutput.innerHTML = html
592
+
593
+ // Add click handlers for clickable errors
594
+ this.parts.testsOutput
595
+ .querySelectorAll('.clickable-error')
596
+ .forEach((el) => {
597
+ el.addEventListener('click', (e) => {
598
+ const line = parseInt(
599
+ (e.currentTarget as HTMLElement).dataset.line || '0',
600
+ 10
601
+ )
602
+ if (line > 0) {
603
+ this.goToSourceLine(line)
604
+ }
605
+ })
606
+ })
607
+ }
608
+
609
+ private updateTestsTabLabel(passed: number, failed: number) {
610
+ const tabs = this.parts.outputTabs
611
+ if (!tabs) return
612
+
613
+ if (failed > 0) {
614
+ tabs.style.setProperty('--test-indicator-color', '#dc2626')
615
+ } else if (passed > 0) {
616
+ tabs.style.setProperty('--test-indicator-color', '#16a34a')
617
+ } else {
618
+ tabs.style.setProperty('--test-indicator-color', 'transparent')
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Format transpile error with helpful context
624
+ */
625
+ private formatTranspileError = (
626
+ e: any,
627
+ source: string
628
+ ): { short: string; detailed: string } => {
629
+ const lines = source.split('\n')
630
+ const line = e.line ?? 1
631
+ const column = e.column ?? 0
632
+ const message = e.message || String(e)
633
+
634
+ // Short version for status bar
635
+ const short = e.line
636
+ ? `Error at line ${line}: ${message}`
637
+ : `Error: ${message}`
638
+
639
+ // Detailed version with code context
640
+ const detailedLines = ['// Transpilation Error', '// ' + '='.repeat(50), '']
641
+
642
+ // Add the error message
643
+ detailedLines.push(`// ${message}`)
644
+ if (e.line) {
645
+ detailedLines.push(`// at line ${line}, column ${column}`)
646
+ }
647
+ detailedLines.push('')
648
+
649
+ // Show code context (3 lines before and after)
650
+ if (e.line && lines.length > 0) {
651
+ detailedLines.push('// Code context:')
652
+ const start = Math.max(0, line - 3)
653
+ const end = Math.min(lines.length, line + 2)
654
+
655
+ for (let i = start; i < end; i++) {
656
+ const lineNum = i + 1
657
+ const prefix = lineNum === line ? '>> ' : ' '
658
+ const lineContent = lines[i] ?? ''
659
+ detailedLines.push(
660
+ `// ${prefix}${lineNum.toString().padStart(3)}: ${lineContent}`
661
+ )
662
+
663
+ // Show caret pointing to error column
664
+ if (lineNum === line && column > 0) {
665
+ const caretPos = 10 + column // account for prefix
666
+ detailedLines.push('// ' + ' '.repeat(caretPos) + '^')
667
+ }
668
+ }
669
+ }
670
+
671
+ // Add suggestions based on common errors
672
+ const suggestions = this.getSuggestions(message, source)
673
+ if (suggestions.length > 0) {
674
+ detailedLines.push('')
675
+ detailedLines.push('// Suggestions:')
676
+ for (const suggestion of suggestions) {
677
+ detailedLines.push(`// - ${suggestion}`)
678
+ }
679
+ }
680
+
681
+ return { short, detailed: detailedLines.join('\n') }
682
+ }
683
+
684
+ /**
685
+ * Get helpful suggestions based on error message
686
+ */
687
+ private getSuggestions = (message: string, source: string): string[] => {
688
+ const suggestions: string[] = []
689
+ const msg = message.toLowerCase()
690
+
691
+ if (msg.includes('unexpected token')) {
692
+ suggestions.push('Check for missing brackets, parentheses, or quotes')
693
+ suggestions.push('TJS uses : for type annotations, = for defaults')
694
+ if (source.includes('=>')) {
695
+ suggestions.push(
696
+ 'Arrow functions are not supported - use function keyword'
697
+ )
698
+ }
699
+ }
700
+
701
+ if (msg.includes('unexpected identifier')) {
702
+ suggestions.push('Check for missing commas between parameters')
703
+ suggestions.push(
704
+ 'Check for typos in keywords (function, return, if, while)'
705
+ )
706
+ }
707
+
708
+ if (msg.includes('unterminated string')) {
709
+ suggestions.push('Check for unmatched quotes')
710
+ suggestions.push('Template literals use backticks (`), not quotes')
711
+ }
712
+
713
+ if (msg.includes('imports are not supported')) {
714
+ suggestions.push('For TJS modules, imports work - this error is for AJS')
715
+ suggestions.push('Make sure the module exists in the store')
716
+ }
717
+
718
+ if (msg.includes('required parameter') && msg.includes('optional')) {
719
+ suggestions.push(
720
+ 'Required parameters (name: type) must come before optional (name = default)'
721
+ )
722
+ }
723
+
724
+ if (msg.includes('duplicate parameter')) {
725
+ suggestions.push('Each parameter must have a unique name')
726
+ }
727
+
728
+ return suggestions
729
+ }
730
+
731
+ /**
732
+ * Extract function metadata from source for autocomplete
733
+ * This runs even when transpilation fails (incomplete code)
734
+ */
735
+ private extractFunctionMetadata = (source: string) => {
736
+ // Match function declarations with TJS syntax
737
+ // function name(param: 'type', param2 = default) -> returnType { ... }
738
+ const funcRegex =
739
+ /function\s+(\w+)\s*\(\s*([^)]*)\s*\)\s*(?:->\s*([^\s{]+))?\s*\{/g
740
+
741
+ const newMetadata: Record<string, any> = {}
742
+ let match
743
+
744
+ while ((match = funcRegex.exec(source)) !== null) {
745
+ const [, funcName, paramsStr, returnType] = match
746
+
747
+ // Parse parameters
748
+ const params: Record<string, any> = {}
749
+ if (paramsStr.trim()) {
750
+ // Split on commas, but be careful of nested structures
751
+ const paramParts = this.splitParams(paramsStr)
752
+
753
+ for (const paramStr of paramParts) {
754
+ const trimmed = paramStr.trim()
755
+ if (!trimmed) continue
756
+
757
+ // Match: name: 'type' or name = default or name: type = default
758
+ const paramMatch = trimmed.match(
759
+ /^(\w+)\s*(?::\s*([^=]+?))?\s*(?:=\s*(.+))?$/
760
+ )
761
+ if (paramMatch) {
762
+ const [, paramName, typeExample, defaultValue] = paramMatch
763
+ const hasDefault = defaultValue !== undefined
764
+ const typeStr = typeExample?.trim() || defaultValue?.trim()
765
+
766
+ params[paramName] = {
767
+ type: this.inferTypeFromExample(typeStr),
768
+ required: !hasDefault && typeExample !== undefined,
769
+ default: hasDefault ? this.parseDefault(defaultValue) : undefined,
770
+ }
771
+ }
772
+ }
773
+ }
774
+
775
+ newMetadata[funcName] = {
776
+ name: funcName,
777
+ params,
778
+ returns: returnType ? this.inferTypeFromExample(returnType) : undefined,
779
+ }
780
+ }
781
+
782
+ this.functionMetadata = newMetadata
783
+ }
784
+
785
+ /**
786
+ * Split parameter string handling nested brackets
787
+ */
788
+ private splitParams = (paramsStr: string): string[] => {
789
+ const result: string[] = []
790
+ let current = ''
791
+ let depth = 0
792
+
793
+ for (const char of paramsStr) {
794
+ if (char === '(' || char === '[' || char === '{') {
795
+ depth++
796
+ current += char
797
+ } else if (char === ')' || char === ']' || char === '}') {
798
+ depth--
799
+ current += char
800
+ } else if (char === ',' && depth === 0) {
801
+ result.push(current)
802
+ current = ''
803
+ } else {
804
+ current += char
805
+ }
806
+ }
807
+ if (current.trim()) {
808
+ result.push(current)
809
+ }
810
+ return result
811
+ }
812
+
813
+ /**
814
+ * Infer type descriptor from example value
815
+ */
816
+ private inferTypeFromExample = (
817
+ example: string | undefined
818
+ ): { kind: string } | undefined => {
819
+ if (!example) return undefined
820
+ const trimmed = example.trim()
821
+
822
+ // String literal
823
+ if (/^['"]/.test(trimmed)) {
824
+ return { kind: 'string' }
825
+ }
826
+ // Number
827
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
828
+ return { kind: 'number' }
829
+ }
830
+ // Boolean
831
+ if (trimmed === 'true' || trimmed === 'false') {
832
+ return { kind: 'boolean' }
833
+ }
834
+ // Null
835
+ if (trimmed === 'null') {
836
+ return { kind: 'null' }
837
+ }
838
+ // Array
839
+ if (trimmed.startsWith('[')) {
840
+ return { kind: 'array' }
841
+ }
842
+ // Object
843
+ if (trimmed.startsWith('{')) {
844
+ return { kind: 'object' }
845
+ }
846
+
847
+ return { kind: 'any' }
848
+ }
849
+
850
+ /**
851
+ * Parse default value to JS value
852
+ */
853
+ private parseDefault = (value: string): any => {
854
+ const trimmed = value.trim()
855
+ try {
856
+ // Try to parse as JSON-like value
857
+ if (
858
+ trimmed === 'true' ||
859
+ trimmed === 'false' ||
860
+ trimmed === 'null' ||
861
+ /^-?\d+(\.\d+)?$/.test(trimmed)
862
+ ) {
863
+ return JSON.parse(trimmed)
864
+ }
865
+ // String literal
866
+ if (/^['"]/.test(trimmed)) {
867
+ return trimmed.slice(1, -1)
868
+ }
869
+ } catch {
870
+ // Return as-is if parsing fails
871
+ }
872
+ return trimmed
873
+ }
874
+
875
+ updateDocs = (_result: any) => {
876
+ const source = this.parts.tjsEditor.value
877
+
878
+ // Use the core generateDocs function to extract all doc blocks and functions
879
+ const docs = generateDocs(source)
880
+
881
+ if (docs.items.length === 0) {
882
+ this.parts.docsOutput.value = '*No documentation available*'
883
+ return
884
+ }
885
+
886
+ this.parts.docsOutput.value = docs.markdown
887
+ }
888
+
889
+ saveModule = async () => {
890
+ const name = this.parts.moduleNameInput.value.trim()
891
+ if (!name) {
892
+ this.parts.statusBar.textContent = 'Enter a module name to save'
893
+ this.parts.statusBar.classList.add('error')
894
+ this.parts.moduleNameInput.focus()
895
+ return
896
+ }
897
+
898
+ // Validate module name format
899
+ if (!/^[a-z][a-z0-9-]*$/i.test(name)) {
900
+ this.parts.statusBar.textContent =
901
+ 'Module name must start with letter, contain only letters, numbers, dashes'
902
+ this.parts.statusBar.classList.add('error')
903
+ return
904
+ }
905
+
906
+ this.parts.statusBar.textContent = 'Validating...'
907
+ this.parts.statusBar.classList.remove('error')
908
+
909
+ try {
910
+ const store = await ModuleStore.open()
911
+ const code = this.parts.tjsEditor.value
912
+
913
+ // Validate first to get detailed results
914
+ const validation = await store.validate(code, 'tjs')
915
+
916
+ if (!validation.valid) {
917
+ // Show validation errors
918
+ const errorMessages = validation.errors.map((e) => e.message).join('; ')
919
+ this.parts.statusBar.textContent = `Save failed: ${errorMessages}`
920
+ this.parts.statusBar.classList.add('error')
921
+
922
+ // Log detailed errors to console
923
+ this.clearConsole()
924
+ this.log('=== Save Validation Failed ===')
925
+ for (const error of validation.errors) {
926
+ if (error.line) {
927
+ this.log(
928
+ `${error.type} error at line ${error.line}: ${error.message}`
929
+ )
930
+ } else {
931
+ this.log(`${error.type} error: ${error.message}`)
932
+ }
933
+ }
934
+ if (validation.warnings.length > 0) {
935
+ this.log('')
936
+ this.log('Warnings:')
937
+ for (const warning of validation.warnings) {
938
+ this.log(` - ${warning}`)
939
+ }
940
+ }
941
+ return
942
+ }
943
+
944
+ // Validation passed, save (skip re-validation)
945
+ await store.save({ name, type: 'tjs', code }, { skipValidation: true })
946
+
947
+ // Success!
948
+ this.parts.statusBar.textContent = `Saved as "${name}"`
949
+ this.parts.statusBar.classList.remove('error')
950
+
951
+ // Show test results if any
952
+ if (validation.testResults && validation.testResults.length > 0) {
953
+ this.clearConsole()
954
+ this.log(`=== Module "${name}" saved successfully ===`)
955
+ this.log('')
956
+ this.log(`Tests: ${validation.testResults.length} passed`)
957
+ for (const test of validation.testResults) {
958
+ this.log(` ✓ ${test.name}`)
959
+ }
960
+ }
961
+ } catch (e: any) {
962
+ this.parts.statusBar.textContent = `Save error: ${e.message}`
963
+ this.parts.statusBar.classList.add('error')
964
+ }
965
+ }
966
+
967
+ run = async () => {
968
+ this.clearConsole()
969
+ this.transpile()
970
+
971
+ if (!this.lastTranspileResult) {
972
+ this.log('Cannot run - transpilation failed')
973
+ return
974
+ }
975
+
976
+ this.parts.statusBar.textContent = 'Running...'
977
+
978
+ try {
979
+ // Build the preview HTML
980
+ const htmlContent = this.parts.htmlEditor.value
981
+ const cssContent = this.parts.cssEditor.value
982
+ const jsCode = this.lastTranspileResult.code
983
+
984
+ // Resolve imports from the transpiled code
985
+ const imports = extractImports(jsCode)
986
+ let importMapScript = ''
987
+
988
+ if (imports.length > 0) {
989
+ this.log(`Resolving imports: ${imports.join(', ')}`)
990
+ const { importMap, errors, localModules } = await resolveImports(jsCode)
991
+
992
+ if (errors.length > 0) {
993
+ for (const err of errors) {
994
+ this.log(`Import error: ${err}`)
995
+ }
996
+ }
997
+
998
+ console.log('[run] importMap:', JSON.stringify(importMap, null, 2))
999
+ if (Object.keys(importMap.imports).length > 0) {
1000
+ importMapScript = `<script type="importmap">${JSON.stringify(
1001
+ importMap
1002
+ )}</script>`
1003
+ console.log('[run] importMapScript:', importMapScript)
1004
+ }
1005
+ }
1006
+
1007
+ // Extract import statements from jsCode - they must be at the top of the module
1008
+ // Matches: import ... from 'pkg', import 'pkg' (side-effect)
1009
+ const importStatements: string[] = []
1010
+ const codeWithoutImports = jsCode.replace(
1011
+ /^import\s+(?:.*?from\s+)?['"][^'"]+['"];?\s*$/gm,
1012
+ (match) => {
1013
+ importStatements.push(match)
1014
+ return ''
1015
+ }
1016
+ )
1017
+
1018
+ // Create a complete HTML document for the iframe
1019
+ const iframeDoc = `<!DOCTYPE html>
1020
+ <html>
1021
+ <head>
1022
+ <style>${cssContent}</style>
1023
+ ${importMapScript}
1024
+ </head>
1025
+ <body>
1026
+ ${htmlContent}
1027
+ <!-- TJS Runtime stub must be set up BEFORE imports execute -->
1028
+ <script>
1029
+ globalThis.__tjs = {
1030
+ version: '0.0.0',
1031
+ pushStack: () => {},
1032
+ popStack: () => {},
1033
+ getStack: () => [],
1034
+ typeError: (path, expected, value) => {
1035
+ const actual = value === null ? 'null' : typeof value;
1036
+ const err = new Error(\`Expected \${expected} for '\${path}', got \${actual}\`);
1037
+ err.name = 'MonadicError';
1038
+ err.path = path;
1039
+ err.expected = expected;
1040
+ err.actual = actual;
1041
+ return err;
1042
+ },
1043
+ createRuntime: function() { return this; },
1044
+ Is: (a, b) => {
1045
+ if (a === b) return true;
1046
+ if (a === null || b === null) return a === b;
1047
+ if (typeof a !== typeof b) return false;
1048
+ if (typeof a !== 'object') return false;
1049
+ if (Array.isArray(a) && Array.isArray(b)) {
1050
+ if (a.length !== b.length) return false;
1051
+ return a.every((v, i) => globalThis.__tjs.Is(v, b[i]));
1052
+ }
1053
+ const keysA = Object.keys(a);
1054
+ const keysB = Object.keys(b);
1055
+ if (keysA.length !== keysB.length) return false;
1056
+ return keysA.every(k => globalThis.__tjs.Is(a[k], b[k]));
1057
+ },
1058
+ IsNot: (a, b) => !globalThis.__tjs.Is(a, b),
1059
+ };
1060
+ </script>
1061
+ <script type="module">
1062
+ // Import statements must be at the top of the module
1063
+ ${importStatements.join('\n ')}
1064
+
1065
+ // Capture console.log
1066
+ const _log = console.log;
1067
+ console.log = (...args) => {
1068
+ _log(...args);
1069
+ parent.postMessage({ type: 'console', message: args.map(a =>
1070
+ typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)
1071
+ ).join(' ') }, '*');
1072
+ };
1073
+
1074
+ try {
1075
+ const __execStart = performance.now();
1076
+ ${codeWithoutImports}
1077
+
1078
+ // Try to call the function if it exists and show result
1079
+ const funcName = Object.keys(window).find(k => {
1080
+ try { return typeof window[k] === 'function' && window[k].__tjs; }
1081
+ catch { return false; }
1082
+ });
1083
+ if (funcName) {
1084
+ const __callStart = performance.now();
1085
+ const result = window[funcName]();
1086
+ const __execTime = performance.now() - __callStart;
1087
+ parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
1088
+ if (result !== undefined) {
1089
+ const output = document.getElementById('output');
1090
+ if (output) {
1091
+ output.textContent = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
1092
+ }
1093
+ console.log('Result:', result);
1094
+ }
1095
+ } else {
1096
+ // No TJS function found, report total parse/exec time
1097
+ const __execTime = performance.now() - __execStart;
1098
+ parent.postMessage({ type: 'timing', execTime: __execTime }, '*');
1099
+ }
1100
+ } catch (e) {
1101
+ parent.postMessage({ type: 'error', message: e.message }, '*');
1102
+ }
1103
+ </script>
1104
+ </body>
1105
+ </html>`
1106
+
1107
+ // Listen for messages from iframe
1108
+ const messageHandler = (event: MessageEvent) => {
1109
+ if (event.data?.type === 'console') {
1110
+ this.log(event.data.message)
1111
+ } else if (event.data?.type === 'timing') {
1112
+ // Update console header with execution time
1113
+ const execTime = event.data.execTime
1114
+ const execStr =
1115
+ execTime < 1
1116
+ ? `${(execTime * 1000).toFixed(0)}μs`
1117
+ : `${execTime.toFixed(2)}ms`
1118
+ this.parts.consoleHeader.textContent = `Console — executed in ${execStr}`
1119
+ } else if (event.data?.type === 'error') {
1120
+ this.log(`Error: ${event.data.message}`)
1121
+ this.parts.statusBar.textContent = 'Runtime error'
1122
+ this.parts.statusBar.classList.add('error')
1123
+ }
1124
+ }
1125
+ window.addEventListener('message', messageHandler)
1126
+
1127
+ // Set iframe content using blob URL instead of srcdoc
1128
+ // This allows import maps to work with external URLs
1129
+ const iframe = this.parts.previewFrame
1130
+ const blob = new Blob([iframeDoc], { type: 'text/html' })
1131
+ const blobUrl = URL.createObjectURL(blob)
1132
+
1133
+ // Clean up previous blob URL if any
1134
+ if (iframe.dataset.blobUrl) {
1135
+ URL.revokeObjectURL(iframe.dataset.blobUrl)
1136
+ }
1137
+ iframe.dataset.blobUrl = blobUrl
1138
+ iframe.src = blobUrl
1139
+
1140
+ // Wait a bit for execution, then clean up listener
1141
+ setTimeout(() => {
1142
+ window.removeEventListener('message', messageHandler)
1143
+ // Don't overwrite status bar - keep showing transpile time
1144
+ }, 1000)
1145
+ } catch (e: any) {
1146
+ this.log(`Error: ${e.message}`)
1147
+ this.parts.statusBar.textContent = 'Error'
1148
+ this.parts.statusBar.classList.add('error')
1149
+ }
1150
+ }
1151
+
1152
+ render(): void {
1153
+ super.render()
1154
+ }
1155
+
1156
+ // Public method to set source code (auto-runs when examples are loaded)
1157
+ setSource(code: string, exampleName?: string) {
1158
+ // Save current edits before switching
1159
+ if (this.currentExampleName) {
1160
+ this.editorCache.set(this.currentExampleName, this.parts.tjsEditor.value)
1161
+ }
1162
+
1163
+ // Update current example tracking
1164
+ this.currentExampleName = exampleName || null
1165
+ this.originalCode = code
1166
+
1167
+ // Check if we have cached edits for this example
1168
+ const cachedCode = exampleName ? this.editorCache.get(exampleName) : null
1169
+ this.parts.tjsEditor.value = cachedCode || code
1170
+
1171
+ // Update revert button visibility
1172
+ this.updateRevertButton()
1173
+
1174
+ // Transpile and run sequentially to avoid race conditions
1175
+ this.transpileAndRun()
1176
+ }
1177
+
1178
+ private transpileAndRun = async () => {
1179
+ const mySeq = this.transpileSeq // Capture current seq before transpile increments it
1180
+ await this.transpileAsync()
1181
+ // Only run if this is still the current transpilation
1182
+ if (this.transpileSeq === mySeq + 1) {
1183
+ await this.run()
1184
+ }
1185
+ }
1186
+
1187
+ // Navigate to a specific line in the source editor
1188
+ goToSourceLine(line: number, column: number = 1) {
1189
+ this.parts.inputTabs.value = 0 // Switch to TJS tab (first tab)
1190
+ // Wait for tab switch and editor resize before scrolling
1191
+ setTimeout(() => {
1192
+ this.parts.tjsEditor.goToLine(line, column)
1193
+ }, 50)
1194
+ }
1195
+
1196
+ // Revert to the original example code
1197
+ revertToOriginal = () => {
1198
+ if (this.currentExampleName) {
1199
+ this.editorCache.delete(this.currentExampleName)
1200
+ }
1201
+ this.parts.tjsEditor.value = this.originalCode
1202
+ this.updateRevertButton()
1203
+ this.transpile()
1204
+ }
1205
+
1206
+ // Update revert button state based on whether code has changed
1207
+ private updateRevertButton() {
1208
+ const hasChanges = this.parts.tjsEditor.value !== this.originalCode
1209
+ this.parts.revertBtn.disabled = !hasChanges
1210
+ this.parts.revertBtn.style.opacity = hasChanges ? '1' : '0.5'
1211
+ }
1212
+ }
1213
+
1214
+ export const tjsPlayground = TJSPlayground.elementCreator({
1215
+ tag: 'tjs-playground',
1216
+ styleSpec: {
1217
+ ':host': {
1218
+ display: 'flex',
1219
+ flexDirection: 'column',
1220
+ height: '100%',
1221
+ flex: '1 1 auto',
1222
+ background: 'var(--background, #fff)',
1223
+ color: 'var(--text-color, #1f2937)',
1224
+ fontFamily: 'system-ui, sans-serif',
1225
+ },
1226
+
1227
+ ':host .tjs-toolbar': {
1228
+ display: 'flex',
1229
+ alignItems: 'center',
1230
+ gap: '10px',
1231
+ padding: '8px 12px',
1232
+ background: 'var(--code-background, #f3f4f6)',
1233
+ borderBottom: '1px solid var(--code-border, #e5e7eb)',
1234
+ },
1235
+
1236
+ ':host .run-btn': {
1237
+ display: 'flex',
1238
+ alignItems: 'center',
1239
+ gap: '4px',
1240
+ padding: '6px 12px',
1241
+ background: 'var(--brand-color, #3d4a6b)',
1242
+ color: 'var(--brand-text-color, white)',
1243
+ border: 'none',
1244
+ borderRadius: '6px',
1245
+ cursor: 'pointer',
1246
+ fontWeight: '500',
1247
+ fontSize: '14px',
1248
+ },
1249
+
1250
+ ':host .run-btn:hover': {
1251
+ filter: 'brightness(1.1)',
1252
+ },
1253
+
1254
+ ':host .toolbar-separator': {
1255
+ width: '1px',
1256
+ height: '20px',
1257
+ background: 'var(--code-border, #d1d5db)',
1258
+ },
1259
+
1260
+ ':host .build-flags': {
1261
+ display: 'flex',
1262
+ alignItems: 'center',
1263
+ gap: '12px',
1264
+ },
1265
+
1266
+ ':host .flag-label': {
1267
+ display: 'flex',
1268
+ alignItems: 'center',
1269
+ gap: '4px',
1270
+ fontSize: '13px',
1271
+ color: 'var(--text-color, #6b7280)',
1272
+ cursor: 'pointer',
1273
+ userSelect: 'none',
1274
+ },
1275
+
1276
+ ':host .flag-label:hover': {
1277
+ color: 'var(--text-color, #374151)',
1278
+ },
1279
+
1280
+ ':host .flag-label input[type="checkbox"]': {
1281
+ margin: '0',
1282
+ cursor: 'pointer',
1283
+ accentColor: 'var(--brand-color, #3d4a6b)',
1284
+ },
1285
+
1286
+ ':host .module-name-input': {
1287
+ padding: '6px 10px',
1288
+ border: '1px solid var(--code-border, #d1d5db)',
1289
+ borderRadius: '6px',
1290
+ fontSize: '14px',
1291
+ fontFamily: 'ui-monospace, monospace',
1292
+ background: 'var(--background, #fff)',
1293
+ color: 'var(--text-color, #1f2937)',
1294
+ width: '160px',
1295
+ },
1296
+
1297
+ ':host .module-name-input:focus': {
1298
+ outline: 'none',
1299
+ borderColor: 'var(--brand-color, #3d4a6b)',
1300
+ boxShadow: '0 0 0 2px rgba(61, 74, 107, 0.2)',
1301
+ },
1302
+
1303
+ ':host .module-name-input::placeholder': {
1304
+ color: 'var(--text-color, #9ca3af)',
1305
+ opacity: '0.6',
1306
+ },
1307
+
1308
+ ':host .save-btn': {
1309
+ display: 'flex',
1310
+ alignItems: 'center',
1311
+ gap: '4px',
1312
+ padding: '6px 12px',
1313
+ background: 'var(--code-background, #e5e7eb)',
1314
+ color: 'var(--text-color, #374151)',
1315
+ border: '1px solid var(--code-border, #d1d5db)',
1316
+ borderRadius: '6px',
1317
+ cursor: 'pointer',
1318
+ fontWeight: '500',
1319
+ fontSize: '14px',
1320
+ },
1321
+
1322
+ ':host .save-btn:hover': {
1323
+ background: 'var(--brand-color, #3d4a6b)',
1324
+ color: 'var(--brand-text-color, white)',
1325
+ borderColor: 'var(--brand-color, #3d4a6b)',
1326
+ },
1327
+
1328
+ ':host .revert-btn': {
1329
+ display: 'flex',
1330
+ alignItems: 'center',
1331
+ gap: '4px',
1332
+ padding: '6px 12px',
1333
+ background: 'var(--code-background, #e5e7eb)',
1334
+ color: 'var(--text-color, #374151)',
1335
+ border: '1px solid var(--code-border, #d1d5db)',
1336
+ borderRadius: '6px',
1337
+ cursor: 'pointer',
1338
+ fontWeight: '500',
1339
+ fontSize: '14px',
1340
+ transition: 'opacity 0.2s',
1341
+ },
1342
+
1343
+ ':host .revert-btn:hover:not(:disabled)': {
1344
+ background: '#fef3c7',
1345
+ borderColor: '#f59e0b',
1346
+ color: '#92400e',
1347
+ },
1348
+
1349
+ ':host .revert-btn:disabled': {
1350
+ cursor: 'default',
1351
+ },
1352
+
1353
+ ':host .elastic': {
1354
+ flex: '1',
1355
+ },
1356
+
1357
+ ':host .status-bar': {
1358
+ fontSize: '13px',
1359
+ color: 'var(--text-color, #6b7280)',
1360
+ opacity: '0.7',
1361
+ },
1362
+
1363
+ ':host .status-bar.error': {
1364
+ color: '#dc2626',
1365
+ opacity: '1',
1366
+ },
1367
+
1368
+ ':host .tjs-main': {
1369
+ display: 'flex',
1370
+ flex: '1 1 auto',
1371
+ minHeight: '0',
1372
+ gap: '1px',
1373
+ background: 'var(--code-border, #e5e7eb)',
1374
+ },
1375
+
1376
+ ':host .tjs-input, :host .tjs-output': {
1377
+ flex: '1 1 50%',
1378
+ minWidth: '0',
1379
+ display: 'flex',
1380
+ flexDirection: 'column',
1381
+ background: 'var(--background, #fff)',
1382
+ overflow: 'hidden',
1383
+ },
1384
+
1385
+ ':host .tjs-input xin-tabs, :host .tjs-output xin-tabs': {
1386
+ flex: '1 1 auto',
1387
+ display: 'flex',
1388
+ flexDirection: 'column',
1389
+ minHeight: '0',
1390
+ },
1391
+
1392
+ // Tab content panels need explicit background for dark mode
1393
+ ':host xin-tabs > [name]': {
1394
+ background: 'var(--background, #fff)',
1395
+ color: 'var(--text-color, #1f2937)',
1396
+ },
1397
+
1398
+ // Editor wrapper - contains the shadow DOM code-mirror component
1399
+ ':host .editor-wrapper': {
1400
+ flex: '1 1 auto',
1401
+ height: '100%',
1402
+ minHeight: '300px',
1403
+ position: 'relative',
1404
+ overflow: 'hidden',
1405
+ },
1406
+
1407
+ // code-mirror is shadow DOM, so we just size it - internal styles are handled by the component
1408
+ ':host .editor-wrapper code-mirror': {
1409
+ display: 'block',
1410
+ position: 'absolute',
1411
+ top: '0',
1412
+ left: '0',
1413
+ right: '0',
1414
+ bottom: '0',
1415
+ },
1416
+
1417
+ ':host .js-output': {
1418
+ margin: '0',
1419
+ padding: '12px',
1420
+ background: 'var(--code-background, #f3f4f6)',
1421
+ color: 'var(--text-color, #1f2937)',
1422
+ fontSize: '13px',
1423
+ fontFamily: 'ui-monospace, monospace',
1424
+ overflow: 'auto',
1425
+ height: '100%',
1426
+ whiteSpace: 'pre-wrap',
1427
+ },
1428
+
1429
+ ':host .preview-container': {
1430
+ height: '100%',
1431
+ background: 'var(--background, #fff)',
1432
+ },
1433
+
1434
+ ':host .preview-frame': {
1435
+ width: '100%',
1436
+ height: '100%',
1437
+ border: 'none',
1438
+ },
1439
+
1440
+ ':host .docs-output': {
1441
+ display: 'block',
1442
+ padding: '12px 16px',
1443
+ fontSize: '14px',
1444
+ fontFamily: 'system-ui, sans-serif',
1445
+ color: 'var(--text-color, inherit)',
1446
+ background: 'var(--background, #fff)',
1447
+ height: '100%',
1448
+ overflow: 'auto',
1449
+ },
1450
+
1451
+ ':host .docs-output h2': {
1452
+ fontSize: '1.25em',
1453
+ marginTop: '0',
1454
+ marginBottom: '0.5em',
1455
+ color: 'var(--text-color, #1f2937)',
1456
+ },
1457
+
1458
+ ':host .docs-output pre': {
1459
+ background: 'var(--code-background, #f3f4f6)',
1460
+ padding: '8px 12px',
1461
+ borderRadius: '6px',
1462
+ overflow: 'auto',
1463
+ fontSize: '13px',
1464
+ },
1465
+
1466
+ ':host .docs-output code': {
1467
+ fontFamily: 'ui-monospace, monospace',
1468
+ fontSize: '0.9em',
1469
+ },
1470
+
1471
+ ':host .docs-output p': {
1472
+ margin: '0.75em 0',
1473
+ lineHeight: '1.5',
1474
+ },
1475
+
1476
+ ':host .docs-output h3': {
1477
+ fontSize: '1em',
1478
+ marginTop: '1em',
1479
+ marginBottom: '0.5em',
1480
+ },
1481
+
1482
+ ':host .docs-output ul': {
1483
+ paddingLeft: '1.5em',
1484
+ margin: '0.5em 0',
1485
+ },
1486
+
1487
+ ':host .docs-output li': {
1488
+ marginBottom: '0.25em',
1489
+ },
1490
+
1491
+ ':host .docs-output hr': {
1492
+ border: 'none',
1493
+ borderTop: '1px solid var(--code-border, #e5e7eb)',
1494
+ margin: '1.5em 0',
1495
+ },
1496
+
1497
+ ':host .tests-output': {
1498
+ padding: '12px',
1499
+ fontSize: '14px',
1500
+ fontFamily: 'system-ui, sans-serif',
1501
+ color: 'var(--text-color, inherit)',
1502
+ background: 'var(--background, #fff)',
1503
+ height: '100%',
1504
+ overflow: 'auto',
1505
+ },
1506
+
1507
+ ':host .test-summary': {
1508
+ marginBottom: '12px',
1509
+ paddingBottom: '8px',
1510
+ borderBottom: '1px solid var(--code-border, #e5e7eb)',
1511
+ },
1512
+
1513
+ ':host .test-failed': {
1514
+ color: '#dc2626',
1515
+ },
1516
+
1517
+ ':host .test-list': {
1518
+ listStyle: 'none',
1519
+ padding: 0,
1520
+ margin: 0,
1521
+ },
1522
+
1523
+ ':host .test-list li': {
1524
+ padding: '4px 0',
1525
+ },
1526
+
1527
+ ':host .test-pass': {
1528
+ color: '#16a34a',
1529
+ },
1530
+
1531
+ ':host .test-fail': {
1532
+ color: '#dc2626',
1533
+ },
1534
+
1535
+ ':host .test-error': {
1536
+ marginLeft: '20px',
1537
+ marginTop: '4px',
1538
+ padding: '8px',
1539
+ background: 'rgba(220, 38, 38, 0.1)',
1540
+ borderRadius: '4px',
1541
+ fontSize: '13px',
1542
+ fontFamily: 'var(--font-mono, monospace)',
1543
+ },
1544
+
1545
+ ':host .clickable-error': {
1546
+ cursor: 'pointer',
1547
+ textDecoration: 'underline',
1548
+ textDecorationStyle: 'dotted',
1549
+ },
1550
+
1551
+ ':host .clickable-error:hover': {
1552
+ background: 'rgba(220, 38, 38, 0.2)',
1553
+ },
1554
+
1555
+ ':host .sig-badge': {
1556
+ fontSize: '11px',
1557
+ padding: '2px 6px',
1558
+ marginLeft: '8px',
1559
+ background: 'rgba(99, 102, 241, 0.1)',
1560
+ color: '#6366f1',
1561
+ borderRadius: '4px',
1562
+ },
1563
+
1564
+ ':host .tjs-console': {
1565
+ height: '120px',
1566
+ borderTop: '1px solid var(--code-border, #e5e7eb)',
1567
+ display: 'flex',
1568
+ flexDirection: 'column',
1569
+ },
1570
+
1571
+ ':host .console-header': {
1572
+ padding: '4px 12px',
1573
+ background: 'var(--code-background, #f3f4f6)',
1574
+ fontSize: '12px',
1575
+ fontWeight: '500',
1576
+ color: 'var(--text-color, #6b7280)',
1577
+ opacity: '0.7',
1578
+ borderBottom: '1px solid var(--code-border, #e5e7eb)',
1579
+ },
1580
+
1581
+ ':host .console-output': {
1582
+ flex: '1',
1583
+ margin: '0',
1584
+ padding: '8px 12px',
1585
+ background: 'var(--code-background, #f3f4f6)',
1586
+ color: 'var(--text-color, #1f2937)',
1587
+ fontSize: '12px',
1588
+ fontFamily: 'ui-monospace, monospace',
1589
+ overflow: 'auto',
1590
+ whiteSpace: 'pre-wrap',
1591
+ },
1592
+
1593
+ ':host .clickable-line': {
1594
+ cursor: 'pointer',
1595
+ color: '#2563eb',
1596
+ textDecoration: 'underline',
1597
+ textDecorationStyle: 'dotted',
1598
+ },
1599
+
1600
+ ':host .clickable-line:hover': {
1601
+ color: '#1d4ed8',
1602
+ background: 'rgba(37, 99, 235, 0.1)',
1603
+ },
1604
+ },
1605
+ }) as ElementCreator<TJSPlayground>