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.
package/demo/src/index.ts CHANGED
@@ -9,14 +9,7 @@
9
9
 
10
10
  import { elements, tosi, bindings, StyleSheet, bind, observe } from 'tosijs'
11
11
 
12
- import {
13
- icons,
14
- sideNav,
15
- SideNav,
16
- sizeBreak,
17
- popMenu,
18
- markdownViewer,
19
- } from 'tosijs-ui'
12
+ import { icons, sideNav, SideNav, popMenu, markdownViewer } from 'tosijs-ui'
20
13
 
21
14
  import { styleSpec } from './style'
22
15
  StyleSheet('demo-style', styleSpec)
@@ -92,14 +85,6 @@ Object.assign(window, { agent, tosijs, tosijsui, demoRuntime, runAgent })
92
85
  import docs from '../docs.json'
93
86
 
94
87
  // Add playgrounds as special pages
95
- const ajsPlaygroundDoc = {
96
- title: '▶ AJS Playground',
97
- filename: 'playground',
98
- text: '',
99
- isPlayground: 'ajs',
100
- pin: 'top',
101
- }
102
-
103
88
  const tjsPlaygroundDoc = {
104
89
  title: '▶ TJS Playground',
105
90
  filename: 'tjs-playground',
@@ -108,8 +93,16 @@ const tjsPlaygroundDoc = {
108
93
  pin: 'top',
109
94
  }
110
95
 
96
+ const ajsPlaygroundDoc = {
97
+ title: '▶ AJS Playground',
98
+ filename: 'playground',
99
+ text: '',
100
+ isPlayground: 'ajs',
101
+ pin: 'top',
102
+ }
103
+
111
104
  // Insert playgrounds at top
112
- const allDocs = [ajsPlaygroundDoc, tjsPlaygroundDoc, ...docs]
105
+ const allDocs = [tjsPlaygroundDoc, ajsPlaygroundDoc, ...docs]
113
106
 
114
107
  const PROJECT = 'tjs-lang'
115
108
  declare const __VERSION__: string
@@ -138,6 +131,8 @@ const { app, prefs, auth } = tosi({
138
131
  currentView: 'home' as 'home' | 'ajs' | 'tjs' | 'ts',
139
132
  currentExample: null as any,
140
133
  openSection: null as string | null,
134
+ splitMode: null as null | 'code' | 'output',
135
+ splitSessionId: '' as string,
141
136
  },
142
137
  auth: {
143
138
  user: null as {
@@ -266,6 +261,11 @@ function syncURLToState() {
266
261
  app.currentView.value = view
267
262
  }
268
263
  if (section) app.openSection.value = section
264
+ const mode = params.get('mode')
265
+ const newSplitMode = mode === 'code' || mode === 'output' ? mode : null
266
+ const sid = params.get('sid') || ''
267
+ app.splitSessionId.value = sid
268
+ app.splitMode.value = newSplitMode
269
269
 
270
270
  if (example) {
271
271
  if (view === 'ts') {
@@ -302,19 +302,26 @@ syncURLToState()
302
302
  window.addEventListener('hashchange', syncURLToState)
303
303
 
304
304
  // State → URL (observe changes, update hash)
305
- observe(/^app\.(currentView|currentExample|openSection)/, () => {
306
- if (_suppressHashUpdate) return
307
- const params = new URLSearchParams()
308
- const view = app.currentView.valueOf() as string
309
- params.set('view', view)
310
- const example = app.currentExample.valueOf() as any
311
- if (example) {
312
- params.set('example', example.name || example.title || '')
305
+ observe(
306
+ /^app\.(currentView|currentExample|openSection|splitMode|splitSessionId)/,
307
+ () => {
308
+ if (_suppressHashUpdate) return
309
+ const params = new URLSearchParams()
310
+ const view = app.currentView.valueOf() as string
311
+ params.set('view', view)
312
+ const example = app.currentExample.valueOf() as any
313
+ if (example) {
314
+ params.set('example', example.name || example.title || '')
315
+ }
316
+ const section = app.openSection.valueOf() as string | null
317
+ if (section) params.set('section', section)
318
+ const splitMode = app.splitMode.valueOf() as string | null
319
+ if (splitMode) params.set('mode', splitMode)
320
+ const sid = app.splitSessionId.valueOf() as string
321
+ if (sid) params.set('sid', sid)
322
+ window.history.replaceState(null, '', `#${params.toString()}`)
313
323
  }
314
- const section = app.openSection.valueOf() as string | null
315
- if (section) params.set('section', section)
316
- window.history.replaceState(null, '', `#${params.toString()}`)
317
- })
324
+ )
318
325
 
319
326
  // Main app
320
327
  const main = document.querySelector('main') as HTMLElement
@@ -387,6 +394,28 @@ if (main) {
387
394
  icons.npm()
388
395
  ),
389
396
 
397
+ // tosijs link
398
+ a(
399
+ {
400
+ class: 'iconic',
401
+ title: 'tosijs',
402
+ target: '_blank',
403
+ href: 'https://tosijs.net',
404
+ },
405
+ icons.tosi()
406
+ ),
407
+
408
+ // tosijs-ui link
409
+ a(
410
+ {
411
+ class: 'iconic',
412
+ title: 'tosijs-ui',
413
+ target: '_blank',
414
+ href: 'https://ui.tosijs.net',
415
+ },
416
+ icons.tosiUi()
417
+ ),
418
+
390
419
  // Settings menu
391
420
  button(
392
421
  {
@@ -572,6 +601,16 @@ if (main) {
572
601
  icon: 'npm',
573
602
  action: () => window.open(app.npmUrl.valueOf(), '_blank'),
574
603
  },
604
+ {
605
+ caption: 'tosijs',
606
+ icon: 'tosi',
607
+ action: () => window.open('https://tosijs.net', '_blank'),
608
+ },
609
+ {
610
+ caption: 'tosijs-ui',
611
+ icon: 'tosiUi',
612
+ action: () => window.open('https://ui.tosijs.net', '_blank'),
613
+ },
575
614
  ],
576
615
  })
577
616
  },
@@ -724,6 +763,47 @@ if (main) {
724
763
  },
725
764
  })
726
765
 
766
+ // Apply split mode from URL on load (for output windows)
767
+ // Ping the partner window — if no pong, revert to normal
768
+ const initialMode = app.splitMode.valueOf() as string | null
769
+ const initialSid = app.splitSessionId.valueOf() as string
770
+ if (
771
+ initialMode &&
772
+ (initialMode === 'code' || initialMode === 'output') &&
773
+ app.currentView.valueOf() === 'tjs'
774
+ ) {
775
+ const probe = new BroadcastChannel('tjs-playground')
776
+ let gotPong = false
777
+ probe.onmessage = (e: MessageEvent) => {
778
+ if (e.data?.type === 'pong' && e.data.sid === initialSid) {
779
+ gotPong = true
780
+ }
781
+ }
782
+ probe.postMessage({ type: 'ping', sid: initialSid })
783
+ setTimeout(() => {
784
+ probe.close()
785
+ if (gotPong) {
786
+ pg.setSplitMode(initialMode, initialSid || undefined)
787
+ } else {
788
+ // Partner is gone — revert to normal
789
+ app.splitMode.value = null
790
+ app.splitSessionId.value = ''
791
+ }
792
+ }, 300)
793
+ }
794
+
795
+ // Listen for split-mode-change from playground
796
+ // The component handles its own setSplitMode; this just syncs URL state
797
+ pg.addEventListener('split-mode-change', ((e: CustomEvent) => {
798
+ if (e.detail?.mode === 'code') {
799
+ app.splitSessionId.value = e.detail.sid
800
+ app.splitMode.value = 'code'
801
+ } else {
802
+ app.splitSessionId.value = ''
803
+ app.splitMode.value = null
804
+ }
805
+ }) as EventListener)
806
+
727
807
  return pg
728
808
  })(),
729
809
 
@@ -762,13 +842,83 @@ if (main) {
762
842
  },
763
843
  })
764
844
 
845
+ // Apply split mode from URL on load (for output windows)
846
+ // Ping the partner window — if no pong, revert to normal
847
+ const tsInitialMode = app.splitMode.valueOf() as string | null
848
+ const tsInitialSid = app.splitSessionId.valueOf() as string
849
+ if (
850
+ tsInitialMode &&
851
+ (tsInitialMode === 'code' || tsInitialMode === 'output') &&
852
+ app.currentView.valueOf() === 'ts'
853
+ ) {
854
+ const probe = new BroadcastChannel('tjs-playground')
855
+ let gotPong = false
856
+ probe.onmessage = (e: MessageEvent) => {
857
+ if (e.data?.type === 'pong' && e.data.sid === tsInitialSid) {
858
+ gotPong = true
859
+ }
860
+ }
861
+ probe.postMessage({ type: 'ping', sid: tsInitialSid })
862
+ setTimeout(() => {
863
+ probe.close()
864
+ if (gotPong) {
865
+ pg.setSplitMode(tsInitialMode, tsInitialSid || undefined)
866
+ } else {
867
+ // Partner is gone — revert to normal
868
+ app.splitMode.value = null
869
+ app.splitSessionId.value = ''
870
+ }
871
+ }, 300)
872
+ }
873
+
874
+ // Listen for split-mode-change from playground
875
+ // The component handles its own setSplitMode; this just syncs URL state
876
+ pg.addEventListener('split-mode-change', ((e: CustomEvent) => {
877
+ if (e.detail?.mode === 'code') {
878
+ app.splitSessionId.value = e.detail.sid
879
+ app.splitMode.value = 'code'
880
+ } else {
881
+ app.splitSessionId.value = ''
882
+ app.splitMode.value = null
883
+ }
884
+ }) as EventListener)
885
+
765
886
  return pg
766
887
  })()
767
888
  )
768
889
  )
769
890
  )
891
+
892
+ // In output mode, hide header and sidebar — just show the playground full-screen
893
+ if (app.splitMode.valueOf() === 'output') {
894
+ const headerEl = main.querySelector('header')
895
+ if (headerEl) headerEl.style.display = 'none'
896
+ const navEl = main.querySelector(SideNav.tagName!) as SideNav | null
897
+ if (navEl) {
898
+ navEl.style.gridTemplateColumns = '0 1fr'
899
+ const navSlot = navEl.querySelector('[slot="nav"]') as HTMLElement | null
900
+ if (navSlot) navSlot.style.display = 'none'
901
+ }
902
+ }
770
903
  }
771
904
 
905
+ // Notify partner window on close/refresh
906
+ window.addEventListener('beforeunload', () => {
907
+ // Find the active playground and notify it to close its channel
908
+ const view = app.currentView.valueOf()
909
+ if (app.splitMode.valueOf()) {
910
+ if (view === 'tjs') {
911
+ const pg = document.querySelector(
912
+ 'tjs-playground'
913
+ ) as TJSPlayground | null
914
+ pg?.notifyClose()
915
+ } else if (view === 'ts') {
916
+ const pg = document.querySelector('ts-playground') as TSPlayground | null
917
+ pg?.notifyClose()
918
+ }
919
+ }
920
+ })
921
+
772
922
  // Log welcome message
773
923
  console.log(
774
924
  `%c tjs-lang %c v${VERSION} `,
@@ -79,6 +79,8 @@ export interface IframeDocOptions {
79
79
  parentBindings?: boolean
80
80
  /** Auto-find and call TJS-annotated functions, append DOM results */
81
81
  autoCallTjsFunction?: boolean
82
+ /** Whether parent is in dark mode — sets color-scheme on iframe */
83
+ darkMode?: boolean
82
84
  }
83
85
 
84
86
  /**
@@ -95,8 +97,11 @@ export function buildIframeDoc(options: IframeDocOptions): string {
95
97
  importStatements = [],
96
98
  parentBindings = false,
97
99
  autoCallTjsFunction = false,
100
+ darkMode = false,
98
101
  } = options
99
102
 
103
+ const colorScheme = darkMode ? 'dark' : 'light dark'
104
+
100
105
  const parentBindingsScript = parentBindings
101
106
  ? `
102
107
  if (parent.run) window.run = parent.run.bind(parent);
@@ -146,6 +151,7 @@ export function buildIframeDoc(options: IframeDocOptions): string {
146
151
  return `<!DOCTYPE html>
147
152
  <html>
148
153
  <head>
154
+ <style>:root { color-scheme: ${colorScheme} }</style>
149
155
  <style>${cssContent}</style>
150
156
  ${importMapScript}
151
157
  </head>
@@ -158,9 +164,9 @@ ${TJS_RUNTIME_STUB}
158
164
  ${importStatements.join('\n ')}
159
165
  ${CONSOLE_CAPTURE_SCRIPT}
160
166
 
161
- const __childrenBefore = document.body.children.length;
167
+ const __childrenBefore = document.body.childNodes.length;
162
168
  try {${executionCode}
163
- if (document.body.children.length > __childrenBefore) {
169
+ if (document.body.childNodes.length > __childrenBefore) {
164
170
  parent.postMessage({ type: 'hasPreviewContent' }, '*');
165
171
  }
166
172
  } catch (e) {
@@ -174,6 +180,7 @@ ${CONSOLE_CAPTURE_SCRIPT}
174
180
  return `<!DOCTYPE html>
175
181
  <html>
176
182
  <head>
183
+ <style>:root { color-scheme: ${colorScheme} }</style>
177
184
  <style>${cssContent}</style>
178
185
  ${importMapScript}
179
186
  </head>
@@ -183,9 +190,9 @@ ${CONSOLE_CAPTURE_SCRIPT}
183
190
  ${TJS_RUNTIME_STUB}
184
191
  ${CONSOLE_CAPTURE_SCRIPT}
185
192
 
186
- const __childrenBefore = document.body.children.length;
193
+ const __childrenBefore = document.body.childNodes.length;
187
194
  try {${executionCode}
188
- if (document.body.children.length > __childrenBefore) {
195
+ if (document.body.childNodes.length > __childrenBefore) {
189
196
  parent.postMessage({ type: 'hasPreviewContent' }, '*');
190
197
  }
191
198
  } catch (e) {
@@ -867,11 +867,11 @@ export class Playground extends Component<PlaygroundParts> {
867
867
  const isHttps = window.location.protocol === 'https:'
868
868
  if (isHttps) {
869
869
  throw new Error(
870
- 'No LLM configured. Go to Settings (⋮) > API Keys to add an OpenAI or Anthropic API key. Note: Local LLM endpoints require HTTP.'
870
+ 'No LLM configured. Go to Settings (⋮) > API Keys to add an API key (OpenAI, Anthropic, Gemini, or Deepseek). Note: Local LLM endpoints require HTTP.'
871
871
  )
872
872
  } else {
873
873
  throw new Error(
874
- 'No LLM configured. Go to Settings (⋮) > API Keys to add an OpenAI key, Anthropic key, or LM Studio endpoint (default: http://localhost:1234/v1).'
874
+ 'No LLM configured. Go to Settings (⋮) > API Keys to add an API key (OpenAI, Anthropic, Gemini, Deepseek) or LM Studio endpoint (default: http://localhost:1234/v1).'
875
875
  )
876
876
  }
877
877
  }