tjs-lang 0.5.2 → 0.5.4

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.
@@ -8,7 +8,7 @@
8
8
  * - TJS Docs (documentation that opens in floating viewer)
9
9
  */
10
10
 
11
- import { Component, elements, ElementCreator, vars } from 'tosijs'
11
+ import { Component, elements, ElementCreator, vars, observe } from 'tosijs'
12
12
  import {
13
13
  xinFloat,
14
14
  XinFloat,
@@ -72,27 +72,12 @@ interface DocItem {
72
72
  requiresApi?: boolean
73
73
  }
74
74
 
75
- interface DemoNavEvents {
76
- 'select-ajs-example': {
77
- example: { name: string; description: string; code: string; group?: string }
78
- }
79
- 'select-tjs-example': {
80
- example: { name: string; description: string; code: string; group?: string }
81
- }
82
- 'select-ts-example': { example: TSExample }
83
- 'select-doc': { doc: DocItem }
84
- }
85
-
86
75
  export class DemoNav extends Component {
87
76
  private _docs: DocItem[] = []
88
- private openSection: string | null = null
77
+ private _appState: any = null // boxed proxy from index.ts
89
78
  private floatViewer: XinFloat | null = null
90
79
  private mdViewer: MarkdownViewer | null = null
91
80
 
92
- // Track current selection for highlighting
93
- private _currentView: 'home' | 'ajs' | 'tjs' = 'home'
94
- private _currentExample: string | null = null
95
-
96
81
  // Computed example arrays from docs
97
82
  private get tjsExamples(): Example[] {
98
83
  return this._docs.filter((d) => d.type === 'example' && d.section === 'tjs')
@@ -101,106 +86,59 @@ export class DemoNav extends Component {
101
86
  private get ajsExamples(): Example[] {
102
87
  return this._docs.filter((d) => d.type === 'example' && d.section === 'ajs')
103
88
  }
104
- constructor() {
105
- super()
106
- // Initialize from URL hash
107
- this.loadStateFromURL()
108
- // Listen for hash changes
109
- window.addEventListener('hashchange', () => this.loadStateFromURL())
110
- }
111
-
112
- get currentView() {
113
- return this._currentView
89
+ get appState() {
90
+ return this._appState
114
91
  }
115
92
 
116
- set currentView(value: 'home' | 'ajs' | 'tjs') {
117
- this._currentView = value
118
- // Auto-open the appropriate section
119
- if (value === 'ajs') {
120
- this.openSection = 'ajs-demos'
121
- } else if (value === 'tjs') {
122
- this.openSection = 'tjs-demos'
123
- }
93
+ set appState(state: any) {
94
+ this._appState = state
95
+ if (!state) return
96
+ // Observe state changes to update nav
97
+ observe(/^app\.(currentView|currentExample)/, () => {
98
+ this.rebuildNav()
99
+ this.updateCurrentIndicator()
100
+ })
101
+ observe('app.openSection', () => {
102
+ this.rebuildNav()
103
+ })
104
+ // Initial sync
124
105
  this.rebuildNav()
125
- // Update indicator after rebuild (DOM now exists)
126
106
  this.updateCurrentIndicator()
127
107
  }
128
108
 
129
- get currentExample() {
130
- return this._currentExample
109
+ private get _currentView(): string {
110
+ return this._appState?.currentView?.valueOf() ?? 'home'
131
111
  }
132
112
 
133
- set currentExample(value: string | null) {
134
- this._currentExample = value
135
- this.updateCurrentIndicator()
113
+ private get _currentExampleName(): string | null {
114
+ const ex = this._appState?.currentExample?.valueOf()
115
+ if (!ex) return null
116
+ return ex.name || ex.title || null
117
+ }
118
+
119
+ private get _openSection(): string | null {
120
+ return this._appState?.openSection?.valueOf() ?? null
136
121
  }
137
122
 
138
123
  private updateCurrentIndicator() {
139
- // Update .current class on nav items
124
+ const exName = this._currentExampleName
140
125
  const items = this.querySelectorAll('.nav-item')
141
126
  items.forEach((item) => {
142
127
  const itemName = item.textContent?.trim()
143
- const isCurrent = itemName === this._currentExample
144
- item.classList.toggle('current', isCurrent)
128
+ item.classList.toggle('current', itemName === exName)
145
129
  })
146
- // Update home link
147
130
  const homeLink = this.querySelector('.home-link')
148
131
  homeLink?.classList.toggle('current', this._currentView === 'home')
149
132
  }
150
133
 
151
- private loadStateFromURL() {
152
- const hash = window.location.hash.slice(1) // Remove '#'
153
- if (!hash) return
154
-
155
- const params = new URLSearchParams(hash)
156
- const section = params.get('section')
157
- const view = params.get('view')
158
- const example = params.get('example')
159
-
160
- // Set view and open appropriate section
161
- if (view === 'ajs') {
162
- this._currentView = 'ajs'
163
- this.openSection = 'ajs-demos'
164
- } else if (view === 'tjs') {
165
- this._currentView = 'tjs'
166
- this.openSection = 'tjs-demos'
167
- } else if (view === 'home') {
168
- this._currentView = 'home'
169
- } else if (
170
- section &&
171
- ['ajs-demos', 'tjs-demos', 'ajs-docs', 'tjs-docs'].includes(section)
172
- ) {
173
- this.openSection = section
174
- }
175
-
176
- // Set current example for highlighting
177
- if (example) {
178
- this._currentExample = example
179
- }
180
-
181
- this.rebuildNav()
182
- this.updateCurrentIndicator()
183
- }
184
-
185
- private saveStateToURL() {
186
- const params = new URLSearchParams(window.location.hash.slice(1))
187
- if (this.openSection) {
188
- params.set('section', this.openSection)
189
- }
190
- const newHash = params.toString()
191
- if (newHash !== window.location.hash.slice(1)) {
192
- window.history.replaceState(null, '', `#${newHash}`)
193
- }
194
- }
195
-
196
134
  get docs(): DocItem[] {
197
135
  return this._docs
198
136
  }
199
137
 
200
138
  set docs(value: DocItem[]) {
201
139
  this._docs = value
202
- // Re-render when docs are set
203
140
  this.rebuildNav()
141
+ this.updateCurrentIndicator()
204
142
  }
205
143
 
206
144
  // Light DOM styles (no static styleSpec)
@@ -432,7 +370,7 @@ export class DemoNav extends Component {
432
370
  // TypeScript Examples (TS -> TJS -> JS pipeline)
433
371
  details(
434
372
  {
435
- open: this.openSection === 'ts-demos',
373
+ open: this._openSection === 'ts-demos',
436
374
  'data-section': 'ts-demos',
437
375
  onToggle: this.handleToggle,
438
376
  },
@@ -458,7 +396,7 @@ export class DemoNav extends Component {
458
396
  // TJS Examples
459
397
  details(
460
398
  {
461
- open: this.openSection === 'tjs-demos',
399
+ open: this._openSection === 'tjs-demos',
462
400
  'data-section': 'tjs-demos',
463
401
  onToggle: this.handleToggle,
464
402
  },
@@ -484,7 +422,7 @@ export class DemoNav extends Component {
484
422
  // AJS Examples
485
423
  details(
486
424
  {
487
- open: this.openSection === 'ajs-demos',
425
+ open: this._openSection === 'ajs-demos',
488
426
  'data-section': 'ajs-demos',
489
427
  onToggle: this.handleToggle,
490
428
  },
@@ -510,7 +448,7 @@ export class DemoNav extends Component {
510
448
  // TJS Docs
511
449
  details(
512
450
  {
513
- open: this.openSection === 'tjs-docs',
451
+ open: this._openSection === 'tjs-docs',
514
452
  'data-section': 'tjs-docs',
515
453
  onToggle: this.handleToggle,
516
454
  },
@@ -535,7 +473,7 @@ export class DemoNav extends Component {
535
473
  // AJS Docs
536
474
  details(
537
475
  {
538
- open: this.openSection === 'ajs-docs',
476
+ open: this._openSection === 'ajs-docs',
539
477
  'data-section': 'ajs-docs',
540
478
  onToggle: this.handleToggle,
541
479
  },
@@ -564,16 +502,13 @@ export class DemoNav extends Component {
564
502
  const section = details.getAttribute('data-section')
565
503
 
566
504
  if (details.open) {
505
+ if (this._appState) this._appState.openSection.value = section
567
506
  // Close other sections (accordion behavior)
568
- this.openSection = section
569
- const allDetails = this.querySelectorAll('details')
570
- allDetails.forEach((d) => {
507
+ this.querySelectorAll('details').forEach((d) => {
571
508
  if (d !== details && d.open) {
572
509
  d.open = false
573
510
  }
574
511
  })
575
- // Save to URL
576
- this.saveStateToURL()
577
512
  }
578
513
  }
579
514
 
@@ -598,50 +533,30 @@ export class DemoNav extends Component {
598
533
  }
599
534
 
600
535
  selectHome() {
601
- this._currentView = 'home'
602
- this._currentExample = null
603
- this.updateCurrentIndicator()
604
- this.dispatchEvent(
605
- new CustomEvent('select-home', {
606
- bubbles: true,
607
- })
608
- )
536
+ if (!this._appState) return
537
+ this._appState.currentView.value = 'home'
538
+ this._appState.currentExample.value = null
609
539
  }
610
540
 
611
541
  selectAjsExample(example: Example) {
612
- this._currentView = 'ajs'
613
- this._currentExample = example.title
614
- this.updateCurrentIndicator()
615
- this.dispatchEvent(
616
- new CustomEvent('select-ajs-example', {
617
- detail: { example: exampleToLegacy(example) },
618
- bubbles: true,
619
- })
620
- )
542
+ if (!this._appState) return
543
+ this._appState.currentView.value = 'ajs'
544
+ this._appState.currentExample.value = exampleToLegacy(example)
545
+ this._appState.openSection.value = 'ajs-demos'
621
546
  }
622
547
 
623
548
  selectTjsExample(example: Example) {
624
- this._currentView = 'tjs'
625
- this._currentExample = example.title
626
- this.updateCurrentIndicator()
627
- this.dispatchEvent(
628
- new CustomEvent('select-tjs-example', {
629
- detail: { example: exampleToLegacy(example) },
630
- bubbles: true,
631
- })
632
- )
549
+ if (!this._appState) return
550
+ this._appState.currentView.value = 'tjs'
551
+ this._appState.currentExample.value = exampleToLegacy(example)
552
+ this._appState.openSection.value = 'tjs-demos'
633
553
  }
634
554
 
635
555
  selectTsExample(example: TSExample) {
636
- this._currentView = 'tjs' // Will switch to 'ts' when TS playground is wired up
637
- this._currentExample = example.name
638
- this.updateCurrentIndicator()
639
- this.dispatchEvent(
640
- new CustomEvent('select-ts-example', {
641
- detail: { example: exampleToLegacy(example) },
642
- bubbles: true,
643
- })
644
- )
556
+ if (!this._appState) return
557
+ this._appState.currentView.value = 'ts'
558
+ this._appState.currentExample.value = exampleToLegacy(example)
559
+ this._appState.openSection.value = 'ts-demos'
645
560
  }
646
561
 
647
562
  selectDoc(doc: DocItem) {
package/demo/src/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * - Markdown content with live examples
8
8
  */
9
9
 
10
- import { elements, tosi, bindings, StyleSheet, bind } from 'tosijs'
10
+ import { elements, tosi, bindings, StyleSheet, bind, observe } from 'tosijs'
11
11
 
12
12
  import {
13
13
  icons,
@@ -137,6 +137,7 @@ const { app, prefs, auth } = tosi({
137
137
  compact: false,
138
138
  currentView: 'home' as 'home' | 'ajs' | 'tjs' | 'ts',
139
139
  currentExample: null as any,
140
+ openSection: null as string | null,
140
141
  },
141
142
  auth: {
142
143
  user: null as {
@@ -248,35 +249,32 @@ window.addEventListener('popstate', () => {
248
249
  app.docs.find((doc: any) => doc.filename === filename) || app.docs[0]
249
250
  })
250
251
 
251
- // URL state management for view and example
252
- function loadViewStateFromURL() {
252
+ // URL state sync (single source of truth: app state)
253
+ let _suppressHashUpdate = false
254
+
255
+ function syncURLToState() {
253
256
  const hash = window.location.hash.slice(1)
254
- if (!hash) {
255
- app.currentView = 'home'
256
- return
257
- }
257
+ if (!hash) return
258
258
 
259
+ _suppressHashUpdate = true
259
260
  const params = new URLSearchParams(hash)
260
261
  const view = params.get('view')
261
262
  const example = params.get('example')
263
+ const section = params.get('section')
262
264
 
263
- if (view === 'home') {
264
- app.currentView.xin = 'home'
265
- app.currentExample = null
266
- } else if (view === 'ajs' || view === 'tjs' || view === 'ts') {
267
- app.currentView = view
265
+ if (view === 'home' || view === 'ajs' || view === 'tjs' || view === 'ts') {
266
+ app.currentView.value = view
268
267
  }
268
+ if (section) app.openSection.value = section
269
269
 
270
270
  if (example) {
271
- // Find example by name in appropriate list
272
271
  if (view === 'ts') {
273
272
  const found = tsExamples.find((e: any) => e.name === example)
274
- if (found) app.currentExample = found
273
+ if (found) app.currentExample.value = found
275
274
  } else if (view === 'tjs') {
276
- // Find in docs by title
277
275
  const found = findExampleInDocs(docs, 'tjs', example)
278
276
  if (found) {
279
- app.currentExample = {
277
+ app.currentExample.value = {
280
278
  name: found.title,
281
279
  description: found.description || found.title,
282
280
  code: found.code || '',
@@ -284,10 +282,9 @@ function loadViewStateFromURL() {
284
282
  }
285
283
  }
286
284
  } else if (view === 'ajs') {
287
- // Find in docs by title
288
285
  const found = findExampleInDocs(docs, 'ajs', example)
289
286
  if (found) {
290
- app.currentExample = {
287
+ app.currentExample.value = {
291
288
  name: found.title,
292
289
  description: found.description || found.title,
293
290
  code: found.code || '',
@@ -295,26 +292,29 @@ function loadViewStateFromURL() {
295
292
  }
296
293
  }
297
294
  }
298
- }
299
- }
300
-
301
- function saveViewStateToURL(view: string, exampleName?: string) {
302
- const params = new URLSearchParams(window.location.hash.slice(1))
303
- params.set('view', view)
304
- if (exampleName) {
305
- params.set('example', exampleName)
306
295
  } else {
307
- params.delete('example')
296
+ app.currentExample.value = null
308
297
  }
309
- const newHash = params.toString()
310
- window.history.replaceState(null, '', `#${newHash}`)
298
+ _suppressHashUpdate = false
311
299
  }
312
300
 
313
- // Load initial state from URL
314
- loadViewStateFromURL()
301
+ syncURLToState()
302
+ window.addEventListener('hashchange', syncURLToState)
315
303
 
316
- // Listen for hash changes
317
- window.addEventListener('hashchange', loadViewStateFromURL)
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 || '')
313
+ }
314
+ const section = app.openSection.valueOf() as string | null
315
+ if (section) params.set('section', section)
316
+ window.history.replaceState(null, '', `#${params.toString()}`)
317
+ })
318
318
 
319
319
  // Main app
320
320
  const main = document.querySelector('main') as HTMLElement
@@ -596,7 +596,7 @@ if (main) {
596
596
  },
597
597
  },
598
598
 
599
- // Demo navigation with 4 accordion sections
599
+ // Demo navigation shares app state directly
600
600
  (() => {
601
601
  const nav = demoNav({
602
602
  slot: 'nav',
@@ -606,41 +606,8 @@ if (main) {
606
606
  },
607
607
  }) as DemoNav
608
608
 
609
- // Pass docs to the nav component
610
609
  nav.docs = docs
611
-
612
- // Handle AJS example selection - load into AJS playground
613
- nav.addEventListener('select-ajs-example', ((event: CustomEvent) => {
614
- const { example } = event.detail
615
- app.currentView = 'ajs'
616
- app.currentExample = example
617
- saveViewStateToURL('ajs', example.name)
618
- }) as EventListener)
619
-
620
- // Handle TJS example selection - load into TJS playground
621
- nav.addEventListener('select-tjs-example', ((event: CustomEvent) => {
622
- const { example } = event.detail
623
- app.currentView = 'tjs'
624
- app.currentExample = example
625
- saveViewStateToURL('tjs', example.name)
626
- }) as EventListener)
627
-
628
- // Handle TS example selection - load into TS playground
629
- nav.addEventListener('select-ts-example', ((event: CustomEvent) => {
630
- const { example } = event.detail
631
- app.currentView = 'ts'
632
- app.currentExample = example
633
- saveViewStateToURL('ts', example.name)
634
- }) as EventListener)
635
-
636
- // Handle Home selection - show README
637
- nav.addEventListener('select-home', (() => {
638
- app.currentView = 'home'
639
- app.currentExample = null
640
- saveViewStateToURL('home')
641
- }) as EventListener)
642
-
643
- // Nav syncs its own state from URL - no need to overwrite on initial load
610
+ nav.appState = app
644
611
 
645
612
  return nav
646
613
  })(),
@@ -81,6 +81,7 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
81
81
  private lastTjsCode: string = ''
82
82
  private lastJsCode: string = ''
83
83
  private consoleMessages: string[] = []
84
+ private _messageHandler: ((e: MessageEvent) => void) | null = null
84
85
 
85
86
  // Editor state persistence
86
87
  private currentExampleName: string | null = null
@@ -514,8 +515,13 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
514
515
  jsCode,
515
516
  })
516
517
 
518
+ // Clean up any previous message handler
519
+ if (this._messageHandler) {
520
+ window.removeEventListener('message', this._messageHandler)
521
+ }
522
+
517
523
  // Listen for messages from iframe
518
- const messageHandler = createIframeMessageHandler({
524
+ this._messageHandler = createIframeMessageHandler({
519
525
  onConsole: (message) => this.log(message),
520
526
  onTiming: (execTime) => {
521
527
  this.parts.consoleHeader.textContent = `Console — executed in ${formatExecTime(
@@ -531,15 +537,19 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
531
537
  this.parts.statusBar.classList.add('error')
532
538
  },
533
539
  })
534
- window.addEventListener('message', messageHandler)
540
+ window.addEventListener('message', this._messageHandler)
535
541
 
536
- // Set iframe content
537
- this.parts.previewFrame.srcdoc = iframeDoc
542
+ // Set iframe content using blob URL instead of srcdoc
543
+ // (srcdoc can cause double-execution in some browsers)
544
+ const iframe = this.parts.previewFrame
545
+ const blob = new Blob([iframeDoc], { type: 'text/html' })
546
+ const blobUrl = URL.createObjectURL(blob)
538
547
 
539
- // Wait a bit for execution, then clean up listener
540
- setTimeout(() => {
541
- window.removeEventListener('message', messageHandler)
542
- }, 1000)
548
+ if (iframe.dataset.blobUrl) {
549
+ URL.revokeObjectURL(iframe.dataset.blobUrl)
550
+ }
551
+ iframe.dataset.blobUrl = blobUrl
552
+ iframe.src = blobUrl
543
553
  } catch (e: any) {
544
554
  this.log(`Error: ${e.message}`)
545
555
  this.parts.statusBar.textContent = 'Error'