tjs-lang 0.5.3 → 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' | 'ts' = '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,113 +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' | 'ts') {
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
- } else if (value === 'ts') {
124
- this.openSection = 'ts-demos'
125
- }
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
126
105
  this.rebuildNav()
127
- // Update indicator after rebuild (DOM now exists)
128
106
  this.updateCurrentIndicator()
129
107
  }
130
108
 
131
- get currentExample() {
132
- return this._currentExample
109
+ private get _currentView(): string {
110
+ return this._appState?.currentView?.valueOf() ?? 'home'
133
111
  }
134
112
 
135
- set currentExample(value: string | null) {
136
- this._currentExample = value
137
- 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
138
121
  }
139
122
 
140
123
  private updateCurrentIndicator() {
141
- // Update .current class on nav items
124
+ const exName = this._currentExampleName
142
125
  const items = this.querySelectorAll('.nav-item')
143
126
  items.forEach((item) => {
144
127
  const itemName = item.textContent?.trim()
145
- const isCurrent = itemName === this._currentExample
146
- item.classList.toggle('current', isCurrent)
128
+ item.classList.toggle('current', itemName === exName)
147
129
  })
148
- // Update home link
149
130
  const homeLink = this.querySelector('.home-link')
150
131
  homeLink?.classList.toggle('current', this._currentView === 'home')
151
132
  }
152
133
 
153
- private loadStateFromURL() {
154
- const hash = window.location.hash.slice(1) // Remove '#'
155
- if (!hash) return
156
-
157
- const params = new URLSearchParams(hash)
158
- const section = params.get('section')
159
- const view = params.get('view')
160
- const example = params.get('example')
161
-
162
- // Set view and open appropriate section
163
- if (view === 'ajs') {
164
- this._currentView = 'ajs'
165
- this.openSection = 'ajs-demos'
166
- } else if (view === 'tjs') {
167
- this._currentView = 'tjs'
168
- this.openSection = 'tjs-demos'
169
- } else if (view === 'ts') {
170
- this._currentView = 'ts'
171
- this.openSection = 'ts-demos'
172
- } else if (view === 'home') {
173
- this._currentView = 'home'
174
- } else if (
175
- section &&
176
- ['ajs-demos', 'tjs-demos', 'ts-demos', 'ajs-docs', 'tjs-docs'].includes(
177
- section
178
- )
179
- ) {
180
- this.openSection = section
181
- }
182
-
183
- // Set current example for highlighting
184
- if (example) {
185
- this._currentExample = example
186
- }
187
-
188
- this.rebuildNav()
189
- this.updateCurrentIndicator()
190
- }
191
-
192
- private saveStateToURL() {
193
- const params = new URLSearchParams(window.location.hash.slice(1))
194
- if (this.openSection) {
195
- params.set('section', this.openSection)
196
- }
197
- const newHash = params.toString()
198
- if (newHash !== window.location.hash.slice(1)) {
199
- window.history.replaceState(null, '', `#${newHash}`)
200
- }
201
- }
202
-
203
134
  get docs(): DocItem[] {
204
135
  return this._docs
205
136
  }
206
137
 
207
138
  set docs(value: DocItem[]) {
208
139
  this._docs = value
209
- // Re-render when docs are set
210
140
  this.rebuildNav()
141
+ this.updateCurrentIndicator()
211
142
  }
212
143
 
213
144
  // Light DOM styles (no static styleSpec)
@@ -439,7 +370,7 @@ export class DemoNav extends Component {
439
370
  // TypeScript Examples (TS -> TJS -> JS pipeline)
440
371
  details(
441
372
  {
442
- open: this.openSection === 'ts-demos',
373
+ open: this._openSection === 'ts-demos',
443
374
  'data-section': 'ts-demos',
444
375
  onToggle: this.handleToggle,
445
376
  },
@@ -465,7 +396,7 @@ export class DemoNav extends Component {
465
396
  // TJS Examples
466
397
  details(
467
398
  {
468
- open: this.openSection === 'tjs-demos',
399
+ open: this._openSection === 'tjs-demos',
469
400
  'data-section': 'tjs-demos',
470
401
  onToggle: this.handleToggle,
471
402
  },
@@ -491,7 +422,7 @@ export class DemoNav extends Component {
491
422
  // AJS Examples
492
423
  details(
493
424
  {
494
- open: this.openSection === 'ajs-demos',
425
+ open: this._openSection === 'ajs-demos',
495
426
  'data-section': 'ajs-demos',
496
427
  onToggle: this.handleToggle,
497
428
  },
@@ -517,7 +448,7 @@ export class DemoNav extends Component {
517
448
  // TJS Docs
518
449
  details(
519
450
  {
520
- open: this.openSection === 'tjs-docs',
451
+ open: this._openSection === 'tjs-docs',
521
452
  'data-section': 'tjs-docs',
522
453
  onToggle: this.handleToggle,
523
454
  },
@@ -542,7 +473,7 @@ export class DemoNav extends Component {
542
473
  // AJS Docs
543
474
  details(
544
475
  {
545
- open: this.openSection === 'ajs-docs',
476
+ open: this._openSection === 'ajs-docs',
546
477
  'data-section': 'ajs-docs',
547
478
  onToggle: this.handleToggle,
548
479
  },
@@ -571,16 +502,13 @@ export class DemoNav extends Component {
571
502
  const section = details.getAttribute('data-section')
572
503
 
573
504
  if (details.open) {
505
+ if (this._appState) this._appState.openSection.value = section
574
506
  // Close other sections (accordion behavior)
575
- this.openSection = section
576
- const allDetails = this.querySelectorAll('details')
577
- allDetails.forEach((d) => {
507
+ this.querySelectorAll('details').forEach((d) => {
578
508
  if (d !== details && d.open) {
579
509
  d.open = false
580
510
  }
581
511
  })
582
- // Save to URL
583
- this.saveStateToURL()
584
512
  }
585
513
  }
586
514
 
@@ -605,50 +533,30 @@ export class DemoNav extends Component {
605
533
  }
606
534
 
607
535
  selectHome() {
608
- this._currentView = 'home'
609
- this._currentExample = null
610
- this.updateCurrentIndicator()
611
- this.dispatchEvent(
612
- new CustomEvent('select-home', {
613
- bubbles: true,
614
- })
615
- )
536
+ if (!this._appState) return
537
+ this._appState.currentView.value = 'home'
538
+ this._appState.currentExample.value = null
616
539
  }
617
540
 
618
541
  selectAjsExample(example: Example) {
619
- this._currentView = 'ajs'
620
- this._currentExample = example.title
621
- this.updateCurrentIndicator()
622
- this.dispatchEvent(
623
- new CustomEvent('select-ajs-example', {
624
- detail: { example: exampleToLegacy(example) },
625
- bubbles: true,
626
- })
627
- )
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'
628
546
  }
629
547
 
630
548
  selectTjsExample(example: Example) {
631
- this._currentView = 'tjs'
632
- this._currentExample = example.title
633
- this.updateCurrentIndicator()
634
- this.dispatchEvent(
635
- new CustomEvent('select-tjs-example', {
636
- detail: { example: exampleToLegacy(example) },
637
- bubbles: true,
638
- })
639
- )
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'
640
553
  }
641
554
 
642
555
  selectTsExample(example: TSExample) {
643
- this._currentView = 'tjs' // Will switch to 'ts' when TS playground is wired up
644
- this._currentExample = example.name
645
- this.updateCurrentIndicator()
646
- this.dispatchEvent(
647
- new CustomEvent('select-ts-example', {
648
- detail: { example: exampleToLegacy(example) },
649
- bubbles: true,
650
- })
651
- )
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'
652
560
  }
653
561
 
654
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'