tjs-lang 0.5.4 → 0.6.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.
@@ -23,7 +23,7 @@ import {
23
23
  // Available modules for autocomplete introspection
24
24
  // These are the actual runtime values that can be introspected
25
25
  import { codeMirror, CodeMirror } from '../../editors/codemirror/component'
26
- import { tjs, type TJSTranspileOptions } from '../../src/lang'
26
+ import { tjs, generateDTS, type TJSTranspileOptions } from '../../src/lang'
27
27
  import { generateDocsMarkdown } from './docs-utils'
28
28
  import { extractImports, generateImportMap, resolveImports } from './imports'
29
29
  import {
@@ -112,7 +112,8 @@ interface TJSPlaygroundParts extends PartsMap {
112
112
  cssEditor: CodeMirror
113
113
  inputTabs: TabSelector
114
114
  outputTabs: TabSelector
115
- jsOutput: HTMLElement
115
+ jsOutput: CodeMirror
116
+ typesOutput: CodeMirror
116
117
  previewFrame: HTMLIFrameElement
117
118
  docsOutput: MarkdownViewer
118
119
  testsOutput: HTMLElement
@@ -128,6 +129,7 @@ interface TJSPlaygroundParts extends PartsMap {
128
129
  debugToggle: HTMLInputElement
129
130
  safetyToggle: HTMLInputElement
130
131
  wasmToggle: HTMLInputElement
132
+ splitBtn: HTMLButtonElement
131
133
  }
132
134
 
133
135
  export class TJSPlayground extends Component<TJSPlaygroundParts> {
@@ -140,6 +142,11 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
140
142
  private originalCode: string = DEFAULT_TJS
141
143
  private editorCache: Map<string, string> = new Map()
142
144
 
145
+ // Split mode
146
+ private _splitMode: null | 'code' | 'output' = null
147
+ private _splitChannel: BroadcastChannel | null = null
148
+ private _splitSessionId: string = ''
149
+
143
150
  // Build flags state
144
151
  private buildFlags = {
145
152
  tests: true, // Run tests at transpile time
@@ -212,6 +219,229 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
212
219
  }
213
220
  }
214
221
 
222
+ /**
223
+ * Set split mode and manage BroadcastChannel.
224
+ * sessionId ties code+output windows together so stale windows are ignored.
225
+ */
226
+ setSplitMode = (mode: null | 'code' | 'output', sessionId?: string) => {
227
+ const prev = this._splitMode
228
+ this._splitMode = mode
229
+ if (sessionId) this._splitSessionId = sessionId
230
+
231
+ // Update button appearance
232
+ this.updateSplitButton()
233
+
234
+ // Toggle visibility of input/output panes directly
235
+ const input = this.querySelector('.tjs-input') as HTMLElement | null
236
+ const output = this.querySelector('.tjs-output') as HTMLElement | null
237
+ const consoleEl = this.querySelector('.tjs-console') as HTMLElement | null
238
+ const flags = this.querySelector('.build-flags') as HTMLElement | null
239
+ const toolbar = this.querySelector('.tjs-toolbar') as HTMLElement | null
240
+
241
+ if (mode === 'code') {
242
+ if (output) output.style.display = 'none'
243
+ if (consoleEl) consoleEl.style.display = 'none'
244
+ if (input) input.style.flex = '1 1 100%'
245
+ } else if (mode === 'output') {
246
+ if (input) input.style.display = 'none'
247
+ if (flags) flags.style.display = 'none'
248
+ if (toolbar) toolbar.style.display = 'none'
249
+ if (output) output.style.flex = '1 1 100%'
250
+ } else {
251
+ // Restore normal layout
252
+ if (input) {
253
+ input.style.display = ''
254
+ input.style.flex = ''
255
+ }
256
+ if (output) {
257
+ output.style.display = ''
258
+ output.style.flex = ''
259
+ }
260
+ if (consoleEl) consoleEl.style.display = ''
261
+ if (flags) flags.style.display = ''
262
+ if (toolbar) toolbar.style.display = ''
263
+ }
264
+
265
+ // Tear down previous channel, notifying partner
266
+ if (this._splitChannel && prev) {
267
+ this._splitChannel.postMessage({
268
+ type: 'closed',
269
+ mode: prev,
270
+ sid: this._splitSessionId,
271
+ })
272
+ this._splitChannel.close()
273
+ this._splitChannel = null
274
+ }
275
+
276
+ if (!mode) {
277
+ this._splitSessionId = ''
278
+ return
279
+ }
280
+
281
+ const sid = this._splitSessionId
282
+ const channel = new BroadcastChannel('tjs-playground')
283
+ this._splitChannel = channel
284
+
285
+ if (mode === 'output') {
286
+ channel.onmessage = (e: MessageEvent) => {
287
+ const msg = e.data
288
+ if (msg.sid !== sid) return // ignore messages from other sessions
289
+ if (msg.type === 'ping') {
290
+ channel.postMessage({ type: 'pong', sid })
291
+ } else if (msg.type === 'code-change' && msg.view === 'tjs') {
292
+ this.parts.tjsEditor.value = msg.source
293
+ this.transpileAndRun()
294
+ } else if (msg.type === 'run' && msg.view === 'tjs') {
295
+ this.run()
296
+ } else if (msg.type === 'closed' && msg.mode === 'code') {
297
+ this.setSplitMode(null)
298
+ this.dispatchEvent(
299
+ new CustomEvent('split-mode-change', {
300
+ detail: null,
301
+ bubbles: true,
302
+ })
303
+ )
304
+ }
305
+ }
306
+ // Request current source from code window
307
+ channel.postMessage({ type: 'request-source', view: 'tjs', sid })
308
+ } else if (mode === 'code') {
309
+ channel.onmessage = (e: MessageEvent) => {
310
+ const msg = e.data
311
+ if (msg.sid !== sid) return // ignore messages from other sessions
312
+ if (msg.type === 'ping') {
313
+ channel.postMessage({ type: 'pong', sid })
314
+ } else if (msg.type === 'request-source' && msg.view === 'tjs') {
315
+ // Output window is asking for current source — send it
316
+ this.broadcastSource()
317
+ } else if (msg.type === 'closed' && msg.mode === 'output') {
318
+ this.setSplitMode(null)
319
+ this.dispatchEvent(
320
+ new CustomEvent('split-mode-change', {
321
+ detail: null,
322
+ bubbles: true,
323
+ })
324
+ )
325
+ }
326
+ }
327
+ // Send initial source to output window immediately
328
+ setTimeout(() => this.broadcastSource(), 50)
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Broadcast editor content to output window (called in code mode)
334
+ */
335
+ private broadcastSource = () => {
336
+ if (this._splitMode === 'code' && this._splitChannel) {
337
+ this._splitChannel.postMessage({
338
+ type: 'code-change',
339
+ view: 'tjs',
340
+ source: this.parts.tjsEditor.value,
341
+ sid: this._splitSessionId,
342
+ })
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Broadcast run command to output window
348
+ */
349
+ broadcastRun = () => {
350
+ if (this._splitMode === 'code' && this._splitChannel) {
351
+ this._splitChannel.postMessage({
352
+ type: 'run',
353
+ view: 'tjs',
354
+ sid: this._splitSessionId,
355
+ })
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Notify partner window that this window is closing
361
+ */
362
+ notifyClose = () => {
363
+ if (this._splitChannel && this._splitMode) {
364
+ this._splitChannel.postMessage({
365
+ type: 'closed',
366
+ mode: this._splitMode,
367
+ sid: this._splitSessionId,
368
+ })
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Handle split/merge button click.
374
+ * window.open must be called directly in the click handler to avoid popup blockers.
375
+ */
376
+ handleSplitClick = () => {
377
+ if (this._splitMode === 'output') {
378
+ // Output window — close it (partner will get 'closed' via beforeunload)
379
+ window.close()
380
+ return
381
+ } else if (this._splitMode === 'code') {
382
+ // Code window — merge back
383
+ this.setSplitMode(null)
384
+ this.dispatchEvent(
385
+ new CustomEvent('split-mode-change', { detail: null, bubbles: true })
386
+ )
387
+ } else {
388
+ // Open output in new window — must happen here in the click handler
389
+ const sid = crypto.randomUUID().slice(0, 8)
390
+ const params = new URLSearchParams(window.location.hash.slice(1))
391
+ params.set('mode', 'output')
392
+ params.set('sid', sid)
393
+ const url = window.location.pathname + '#' + params.toString()
394
+ const win = window.open(url, `tjs-output-${sid}`)
395
+ if (!win) return // popup blocked — do nothing
396
+
397
+ // Apply split mode directly — don't wait for event round-trip
398
+ this.setSplitMode('code', sid)
399
+
400
+ // Also notify parent so it can update URL state
401
+ this.dispatchEvent(
402
+ new CustomEvent('split-mode-change', {
403
+ detail: { mode: 'code', sid },
404
+ bubbles: true,
405
+ })
406
+ )
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Update split button appearance based on current mode
412
+ */
413
+ private updateSplitButton = () => {
414
+ const btn = this._splitBtn
415
+ if (!btn) return
416
+
417
+ btn.innerHTML = ''
418
+ if (this._splitMode === 'output') {
419
+ // Output window — show x to close
420
+ btn.style.display = ''
421
+ btn.classList.remove('split-btn-flip')
422
+ btn.title = 'Close output window'
423
+ btn.append(icons.x({ size: 16 }))
424
+ } else if (this._splitMode === 'code') {
425
+ // Code window — hide (no button needed, output window has the x)
426
+ btn.style.display = 'none'
427
+ } else {
428
+ // Normal — show copy icon (CSS flips it)
429
+ btn.style.display = ''
430
+ btn.classList.add('split-btn-flip')
431
+ btn.title = 'Open output in new window'
432
+ btn.append(icons.copy({ size: 16 }))
433
+ }
434
+ }
435
+
436
+ // Cache the split button reference after first access
437
+ private get _splitBtn(): HTMLButtonElement | null {
438
+ try {
439
+ return this.parts.splitBtn
440
+ } catch {
441
+ return null
442
+ }
443
+ }
444
+
215
445
  content = () => [
216
446
  // Toolbar
217
447
  div(
@@ -334,13 +564,18 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
334
564
  { class: 'tjs-output' },
335
565
  tabSelector(
336
566
  { part: 'outputTabs', style: { height: '100%' } },
337
- div(
338
- { name: 'JS' },
339
- pre(
340
- { part: 'jsOutput', class: 'js-output' },
341
- '// Transpiled JavaScript will appear here'
342
- )
343
- ),
567
+ codeMirror({
568
+ name: 'JS',
569
+ part: 'jsOutput',
570
+ mode: 'javascript',
571
+ disabled: true,
572
+ }),
573
+ codeMirror({
574
+ name: 'Types',
575
+ part: 'typesOutput',
576
+ mode: 'typescript',
577
+ disabled: true,
578
+ }),
344
579
  elements.iframe({
345
580
  name: 'Preview',
346
581
  part: 'previewFrame',
@@ -374,6 +609,16 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
374
609
  { part: 'testsOutput', class: 'tests-output' },
375
610
  'Test results will appear here'
376
611
  )
612
+ ),
613
+ button(
614
+ {
615
+ part: 'splitBtn',
616
+ slot: 'after-tabs',
617
+ class: 'split-btn split-btn-flip',
618
+ title: 'Open output in new window',
619
+ onClick: this.handleSplitClick,
620
+ },
621
+ icons.copy({ size: 16 })
377
622
  )
378
623
  )
379
624
  )
@@ -416,6 +661,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
416
661
  debounceTimer = setTimeout(() => {
417
662
  this.transpile()
418
663
  this.updateRevertButton()
664
+ this.broadcastSource()
419
665
  }, 300)
420
666
 
421
667
  // Update autocomplete context more frequently
@@ -517,7 +763,15 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
517
763
  this.lastTranspileTime = performance.now() - startTime
518
764
 
519
765
  this.lastTranspileResult = result
520
- this.parts.jsOutput.textContent = result.code
766
+ this.parts.jsOutput.value = result.code
767
+
768
+ // Update .d.ts types
769
+ try {
770
+ const dts = generateDTS(result, source)
771
+ this.parts.typesOutput.value = dts
772
+ } catch {
773
+ this.parts.typesOutput.value = '// Could not generate types'
774
+ }
521
775
 
522
776
  // Update docs
523
777
  this.updateDocs(result)
@@ -545,12 +799,13 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
545
799
  } catch (e: any) {
546
800
  // Format error with location info if available
547
801
  const errorInfo = this.formatTranspileError(e, source)
548
- this.parts.jsOutput.textContent = errorInfo.detailed
802
+ this.parts.jsOutput.value = errorInfo.detailed
549
803
  this.parts.statusBar.textContent = errorInfo.short
550
804
  this.parts.statusBar.classList.add('error')
551
805
  this.lastTranspileResult = null
552
806
  // Clear test results on error
553
807
  this.parts.testsOutput.textContent = 'Transpilation failed - no tests run'
808
+ this.parts.typesOutput.value = '// Transpilation failed'
554
809
 
555
810
  // Set error marker in gutter
556
811
  if (e.line) {
@@ -954,6 +1209,11 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
954
1209
  }
955
1210
 
956
1211
  run = async () => {
1212
+ // In code mode, broadcast run to output window instead of running locally
1213
+ if (this._splitMode === 'code') {
1214
+ this.broadcastRun()
1215
+ }
1216
+
957
1217
  this.parts.runBtn.disabled = true
958
1218
  this.clearConsole()
959
1219
  this.transpile()
@@ -1034,6 +1294,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
1034
1294
  importStatements,
1035
1295
  parentBindings: true,
1036
1296
  autoCallTjsFunction: true,
1297
+ darkMode: document.body.classList.contains('darkmode'),
1037
1298
  })
1038
1299
 
1039
1300
  // Listen for messages from iframe
@@ -1244,5 +1505,27 @@ export const tjsPlayground = TJSPlayground.elementCreator({
1244
1505
  display: 'flex',
1245
1506
  flexDirection: 'column',
1246
1507
  },
1508
+
1509
+ // Split button in tab bar
1510
+ ':host .split-btn': {
1511
+ display: 'flex',
1512
+ alignItems: 'center',
1513
+ alignSelf: 'center',
1514
+ padding: '4px 6px',
1515
+ marginRight: '4px',
1516
+ background: 'transparent',
1517
+ border: 'none',
1518
+ cursor: 'pointer',
1519
+ color: 'var(--text-color, #6b7280)',
1520
+ borderRadius: '4px',
1521
+ opacity: '0.7',
1522
+ },
1523
+ ':host .split-btn:hover': {
1524
+ opacity: '1',
1525
+ background: 'var(--code-background, #f3f4f6)',
1526
+ },
1527
+ ':host .split-btn-flip svg': {
1528
+ transform: 'scaleY(-1)',
1529
+ },
1247
1530
  },
1248
1531
  }) as ElementCreator<TJSPlayground>
@@ -75,6 +75,7 @@ interface TSPlaygroundParts extends PartsMap {
75
75
  testsToggle: HTMLInputElement
76
76
  debugToggle: HTMLInputElement
77
77
  safetyToggle: HTMLInputElement
78
+ splitBtn: HTMLButtonElement
78
79
  }
79
80
 
80
81
  export class TSPlayground extends Component<TSPlaygroundParts> {
@@ -88,6 +89,11 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
88
89
  private originalCode: string = DEFAULT_TS
89
90
  private editorCache: Map<string, string> = new Map()
90
91
 
92
+ // Split mode
93
+ private _splitMode: null | 'code' | 'output' = null
94
+ private _splitChannel: BroadcastChannel | null = null
95
+ private _splitSessionId: string = ''
96
+
91
97
  // Build flags state
92
98
  private buildFlags = {
93
99
  tests: true, // Run tests at transpile time
@@ -95,6 +101,200 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
95
101
  safe: true, // Safe mode (validates inputs)
96
102
  }
97
103
 
104
+ /**
105
+ * Set split mode and manage BroadcastChannel
106
+ */
107
+ setSplitMode = (mode: null | 'code' | 'output', sessionId?: string) => {
108
+ const prev = this._splitMode
109
+ this._splitMode = mode
110
+ if (sessionId) this._splitSessionId = sessionId
111
+
112
+ this.updateSplitButton()
113
+
114
+ const input = this.querySelector('.ts-input') as HTMLElement | null
115
+ const output = this.querySelector('.ts-output') as HTMLElement | null
116
+ const tsConsole = this.querySelector('.ts-console') as HTMLElement | null
117
+ const flags = this.querySelector('.build-flags') as HTMLElement | null
118
+ const toolbar = this.querySelector('.ts-toolbar') as HTMLElement | null
119
+
120
+ if (mode === 'code') {
121
+ if (output) output.style.display = 'none'
122
+ if (tsConsole) tsConsole.style.display = 'none'
123
+ if (input) input.style.flex = '1 1 100%'
124
+ } else if (mode === 'output') {
125
+ if (input) input.style.display = 'none'
126
+ if (flags) flags.style.display = 'none'
127
+ if (toolbar) toolbar.style.display = 'none'
128
+ if (output) output.style.flex = '1 1 100%'
129
+ } else {
130
+ if (input) {
131
+ input.style.display = ''
132
+ input.style.flex = ''
133
+ }
134
+ if (output) {
135
+ output.style.display = ''
136
+ output.style.flex = ''
137
+ }
138
+ if (tsConsole) tsConsole.style.display = ''
139
+ if (flags) flags.style.display = ''
140
+ if (toolbar) toolbar.style.display = ''
141
+ }
142
+
143
+ if (this._splitChannel && prev) {
144
+ this._splitChannel.postMessage({
145
+ type: 'closed',
146
+ mode: prev,
147
+ sid: this._splitSessionId,
148
+ })
149
+ this._splitChannel.close()
150
+ this._splitChannel = null
151
+ }
152
+
153
+ if (!mode) {
154
+ this._splitSessionId = ''
155
+ return
156
+ }
157
+
158
+ const sid = this._splitSessionId
159
+ const channel = new BroadcastChannel('tjs-playground')
160
+ this._splitChannel = channel
161
+
162
+ if (mode === 'output') {
163
+ channel.onmessage = (e: MessageEvent) => {
164
+ const msg = e.data
165
+ if (msg.sid !== sid) return
166
+ if (msg.type === 'ping') {
167
+ channel.postMessage({ type: 'pong', sid })
168
+ } else if (msg.type === 'code-change' && msg.view === 'ts') {
169
+ this.parts.tsEditor.value = msg.source
170
+ this.transpile().then(() => this.run())
171
+ } else if (msg.type === 'run' && msg.view === 'ts') {
172
+ this.run()
173
+ } else if (msg.type === 'closed' && msg.mode === 'code') {
174
+ this.setSplitMode(null)
175
+ this.dispatchEvent(
176
+ new CustomEvent('split-mode-change', {
177
+ detail: null,
178
+ bubbles: true,
179
+ })
180
+ )
181
+ }
182
+ }
183
+ // Request current source from code window
184
+ channel.postMessage({ type: 'request-source', view: 'ts', sid })
185
+ } else if (mode === 'code') {
186
+ channel.onmessage = (e: MessageEvent) => {
187
+ const msg = e.data
188
+ if (msg.sid !== sid) return
189
+ if (msg.type === 'ping') {
190
+ channel.postMessage({ type: 'pong', sid })
191
+ } else if (msg.type === 'request-source' && msg.view === 'ts') {
192
+ // Output window is asking for current source — send it
193
+ this.broadcastSource()
194
+ } else if (msg.type === 'closed' && msg.mode === 'output') {
195
+ this.setSplitMode(null)
196
+ this.dispatchEvent(
197
+ new CustomEvent('split-mode-change', {
198
+ detail: null,
199
+ bubbles: true,
200
+ })
201
+ )
202
+ }
203
+ }
204
+ // Send initial source to output window immediately
205
+ setTimeout(() => this.broadcastSource(), 50)
206
+ }
207
+ }
208
+
209
+ private broadcastSource = () => {
210
+ if (this._splitMode === 'code' && this._splitChannel) {
211
+ this._splitChannel.postMessage({
212
+ type: 'code-change',
213
+ view: 'ts',
214
+ source: this.parts.tsEditor.value,
215
+ sid: this._splitSessionId,
216
+ })
217
+ }
218
+ }
219
+
220
+ broadcastRun = () => {
221
+ if (this._splitMode === 'code' && this._splitChannel) {
222
+ this._splitChannel.postMessage({
223
+ type: 'run',
224
+ view: 'ts',
225
+ sid: this._splitSessionId,
226
+ })
227
+ }
228
+ }
229
+
230
+ notifyClose = () => {
231
+ if (this._splitChannel && this._splitMode) {
232
+ this._splitChannel.postMessage({
233
+ type: 'closed',
234
+ mode: this._splitMode,
235
+ sid: this._splitSessionId,
236
+ })
237
+ }
238
+ }
239
+
240
+ handleSplitClick = () => {
241
+ if (this._splitMode === 'output') {
242
+ window.close()
243
+ return
244
+ } else if (this._splitMode === 'code') {
245
+ this.setSplitMode(null)
246
+ this.dispatchEvent(
247
+ new CustomEvent('split-mode-change', { detail: null, bubbles: true })
248
+ )
249
+ } else {
250
+ const sid = crypto.randomUUID().slice(0, 8)
251
+ const params = new URLSearchParams(window.location.hash.slice(1))
252
+ params.set('mode', 'output')
253
+ params.set('sid', sid)
254
+ const url = window.location.pathname + '#' + params.toString()
255
+ const win = window.open(url, `ts-output-${sid}`)
256
+ if (!win) return
257
+
258
+ // Apply split mode directly
259
+ this.setSplitMode('code', sid)
260
+
261
+ this.dispatchEvent(
262
+ new CustomEvent('split-mode-change', {
263
+ detail: { mode: 'code', sid },
264
+ bubbles: true,
265
+ })
266
+ )
267
+ }
268
+ }
269
+
270
+ private updateSplitButton = () => {
271
+ const btn = this._splitBtn
272
+ if (!btn) return
273
+
274
+ btn.innerHTML = ''
275
+ if (this._splitMode === 'output') {
276
+ btn.style.display = ''
277
+ btn.classList.remove('split-btn-flip')
278
+ btn.title = 'Close output window'
279
+ btn.append(icons.x({ size: 16 }))
280
+ } else if (this._splitMode === 'code') {
281
+ btn.style.display = 'none'
282
+ } else {
283
+ btn.style.display = ''
284
+ btn.classList.add('split-btn-flip')
285
+ btn.title = 'Open output in new window'
286
+ btn.append(icons.copy({ size: 16 }))
287
+ }
288
+ }
289
+
290
+ private get _splitBtn(): HTMLButtonElement | null {
291
+ try {
292
+ return this.parts.splitBtn
293
+ } catch {
294
+ return null
295
+ }
296
+ }
297
+
98
298
  content = () => [
99
299
  // Toolbar
100
300
  div(
@@ -251,6 +451,16 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
251
451
  { part: 'testsOutput', class: 'tests-output' },
252
452
  'Test results will appear here'
253
453
  )
454
+ ),
455
+ button(
456
+ {
457
+ part: 'splitBtn',
458
+ slot: 'after-tabs',
459
+ class: 'split-btn split-btn-flip',
460
+ title: 'Open output in new window',
461
+ onClick: this.handleSplitClick,
462
+ },
463
+ icons.copy({ size: 16 })
254
464
  )
255
465
  )
256
466
  )
@@ -284,6 +494,7 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
284
494
  debounceTimer = setTimeout(() => {
285
495
  this.transpile()
286
496
  this.updateRevertButton()
497
+ this.broadcastSource()
287
498
  }, 300)
288
499
  })
289
500
  }
@@ -468,6 +679,11 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
468
679
  }
469
680
 
470
681
  run = async () => {
682
+ // In code mode, broadcast run to output window
683
+ if (this._splitMode === 'code') {
684
+ this.broadcastRun()
685
+ }
686
+
471
687
  this.clearConsole()
472
688
  await this.transpile()
473
689
 
@@ -513,6 +729,7 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
513
729
  htmlContent,
514
730
  importMapScript,
515
731
  jsCode,
732
+ darkMode: document.body.classList.contains('darkmode'),
516
733
  })
517
734
 
518
735
  // Clean up any previous message handler
@@ -696,5 +913,27 @@ export const tsPlayground = TSPlayground.elementCreator({
696
913
  display: 'flex',
697
914
  flexDirection: 'column',
698
915
  },
916
+
917
+ // Split button in tab bar
918
+ ':host .split-btn': {
919
+ display: 'flex',
920
+ alignItems: 'center',
921
+ alignSelf: 'center',
922
+ padding: '4px 6px',
923
+ marginRight: '4px',
924
+ background: 'transparent',
925
+ border: 'none',
926
+ cursor: 'pointer',
927
+ color: 'var(--text-color, #6b7280)',
928
+ borderRadius: '4px',
929
+ opacity: '0.7',
930
+ },
931
+ ':host .split-btn:hover': {
932
+ opacity: '1',
933
+ background: 'var(--code-background, #f3f4f6)',
934
+ },
935
+ ':host .split-btn-flip svg': {
936
+ transform: 'scaleY(-1)',
937
+ },
699
938
  },
700
939
  }) as ElementCreator<TSPlayground>