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.
@@ -8,7 +8,7 @@
8
8
  * - TJS Docs (documentation that opens in floating viewer)
9
9
  */
10
10
 
11
- import { Component, elements, ElementCreator, vars, observe } from 'tosijs'
11
+ import { Component, elements, ElementCreator, vars, bind } from 'tosijs'
12
12
  import {
13
13
  xinFloat,
14
14
  XinFloat,
@@ -75,6 +75,7 @@ interface DocItem {
75
75
  export class DemoNav extends Component {
76
76
  private _docs: DocItem[] = []
77
77
  private _appState: any = null // boxed proxy from index.ts
78
+ private _built = false
78
79
  private floatViewer: XinFloat | null = null
79
80
  private mdViewer: MarkdownViewer | null = null
80
81
 
@@ -93,42 +94,7 @@ export class DemoNav extends Component {
93
94
  set appState(state: any) {
94
95
  this._appState = state
95
96
  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
105
- this.rebuildNav()
106
- this.updateCurrentIndicator()
107
- }
108
-
109
- private get _currentView(): string {
110
- return this._appState?.currentView?.valueOf() ?? 'home'
111
- }
112
-
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
121
- }
122
-
123
- private updateCurrentIndicator() {
124
- const exName = this._currentExampleName
125
- const items = this.querySelectorAll('.nav-item')
126
- items.forEach((item) => {
127
- const itemName = item.textContent?.trim()
128
- item.classList.toggle('current', itemName === exName)
129
- })
130
- const homeLink = this.querySelector('.home-link')
131
- homeLink?.classList.toggle('current', this._currentView === 'home')
97
+ this.tryBuild()
132
98
  }
133
99
 
134
100
  get docs(): DocItem[] {
@@ -137,8 +103,16 @@ export class DemoNav extends Component {
137
103
 
138
104
  set docs(value: DocItem[]) {
139
105
  this._docs = value
140
- this.rebuildNav()
141
- this.updateCurrentIndicator()
106
+ this.tryBuild()
107
+ }
108
+
109
+ // Build nav once when both docs and appState are available
110
+ private tryBuild() {
111
+ if (this._built || !this._appState || !this._docs.length) return
112
+ const container = this.querySelector('.nav-sections')
113
+ if (!container) return
114
+ this._built = true
115
+ this.buildNav(container)
142
116
  }
143
117
 
144
118
  // Light DOM styles (no static styleSpec)
@@ -273,9 +247,7 @@ export class DemoNav extends Component {
273
247
 
274
248
  connectedCallback() {
275
249
  super.connectedCallback()
276
- this.rebuildNav()
277
- // Update indicator after DOM is ready
278
- this.updateCurrentIndicator()
250
+ this.tryBuild()
279
251
  }
280
252
 
281
253
  // Group labels for display
@@ -316,199 +288,156 @@ export class DemoNav extends Component {
316
288
  >(examples: T[], renderItem: (ex: T) => HTMLElement): HTMLElement[] {
317
289
  const grouped = new Map<string, T[]>()
318
290
 
319
- // Group examples
320
291
  for (const ex of examples) {
321
292
  const group = ex.group || 'other'
322
- if (!grouped.has(group)) {
323
- grouped.set(group, [])
324
- }
293
+ if (!grouped.has(group)) grouped.set(group, [])
325
294
  grouped.get(group)!.push(ex)
326
295
  }
327
296
 
328
- // Sort groups by GROUP_ORDER
329
297
  const sortedGroups = Array.from(grouped.keys()).sort((a, b) => {
330
298
  const orderA = DemoNav.GROUP_ORDER.indexOf(a)
331
299
  const orderB = DemoNav.GROUP_ORDER.indexOf(b)
332
300
  return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB)
333
301
  })
334
302
 
335
- // Render groups with headers
336
- const elements: HTMLElement[] = []
303
+ const elts: HTMLElement[] = []
337
304
  for (const group of sortedGroups) {
338
305
  const items = grouped.get(group)!
339
306
  const label = DemoNav.GROUP_LABELS[group] || group
340
-
341
- // Add group header
342
- elements.push(div({ class: 'group-header' }, label))
343
-
344
- // Add items in this group
345
- for (const ex of items) {
346
- elements.push(renderItem(ex))
347
- }
307
+ elts.push(div({ class: 'group-header' }, label))
308
+ for (const ex of items) elts.push(renderItem(ex))
348
309
  }
310
+ return elts
311
+ }
349
312
 
350
- return elements
313
+ // Create a nav item bound to currentExample for highlighting
314
+ private boundNavItem(
315
+ name: string,
316
+ extra: { description?: string; requiresApi?: boolean },
317
+ onClick: () => void
318
+ ): HTMLElement {
319
+ const baseClass = extra.requiresApi ? 'nav-item requires-api' : 'nav-item'
320
+ const item = div(
321
+ {
322
+ class: baseClass,
323
+ title: extra.description,
324
+ 'data-name': name,
325
+ onClick,
326
+ },
327
+ name
328
+ )
329
+ bind(item, 'app.currentExample', {
330
+ toDOM(el: HTMLElement, example: any) {
331
+ const current = example?.name || example?.title || null
332
+ el.classList.toggle('current', current === name)
333
+ },
334
+ })
335
+ return item
351
336
  }
352
337
 
353
- rebuildNav() {
354
- const container = this.querySelector('.nav-sections')
355
- if (!container) return
338
+ // Create a details section bound to openSection for open/close
339
+ private boundSection(
340
+ sectionId: string,
341
+ icon: Element,
342
+ label: string,
343
+ children: HTMLElement[]
344
+ ): HTMLElement {
345
+ const det = details(
346
+ {
347
+ 'data-section': sectionId,
348
+ onToggle: this.handleToggle,
349
+ },
350
+ summary(span({ class: 'section-icon' }, icon), label),
351
+ div({ class: 'section-content' }, ...children)
352
+ )
353
+ bind(det, 'app.openSection', {
354
+ toDOM(el: HTMLDetailsElement, section: string | null) {
355
+ el.open = section === sectionId
356
+ },
357
+ })
358
+ return det
359
+ }
356
360
 
357
- container.innerHTML = ''
358
- container.append(
359
- // Home link
360
- div(
361
- {
362
- class:
363
- this._currentView === 'home' ? 'home-link current' : 'home-link',
364
- onClick: () => this.selectHome(),
365
- },
366
- span({ class: 'section-icon' }, icons.home({ size: 16 })),
367
- 'Home'
368
- ),
361
+ // Build the nav once — all reactive updates via bind
362
+ private buildNav(container: Element) {
363
+ const homeLink = div(
364
+ {
365
+ class: 'home-link',
366
+ onClick: () => this.selectHome(),
367
+ },
368
+ span({ class: 'section-icon' }, icons.home({ size: 16 })),
369
+ 'Home'
370
+ )
371
+ bind(homeLink, 'app.currentView', {
372
+ toDOM(el: HTMLElement, view: string) {
373
+ el.classList.toggle('current', view === 'home')
374
+ },
375
+ })
369
376
 
370
- // TypeScript Examples (TS -> TJS -> JS pipeline)
371
- details(
372
- {
373
- open: this._openSection === 'ts-demos',
374
- 'data-section': 'ts-demos',
375
- onToggle: this.handleToggle,
376
- },
377
- summary(
378
- span({ class: 'section-icon' }, icons.code({ size: 16 })),
379
- 'TypeScript Examples'
380
- ),
381
- div(
382
- { class: 'section-content' },
383
- ...this.renderGroupedExamples(tsExamples, (ex) =>
384
- div(
385
- {
386
- class: 'nav-item',
387
- title: ex.description,
388
- onClick: () => this.selectTsExample(ex),
389
- },
390
- ex.name
391
- )
392
- )
377
+ container.append(
378
+ homeLink,
379
+
380
+ this.boundSection(
381
+ 'ts-demos',
382
+ icons.code({ size: 16 }),
383
+ 'TypeScript Examples',
384
+ this.renderGroupedExamples(tsExamples, (ex) =>
385
+ this.boundNavItem(ex.name, ex, () => this.selectTsExample(ex))
393
386
  )
394
387
  ),
395
388
 
396
- // TJS Examples
397
- details(
398
- {
399
- open: this._openSection === 'tjs-demos',
400
- 'data-section': 'tjs-demos',
401
- onToggle: this.handleToggle,
402
- },
403
- summary(
404
- span({ class: 'section-icon' }, icons.code({ size: 16 })),
405
- 'TJS Examples'
406
- ),
407
- div(
408
- { class: 'section-content' },
409
- ...this.renderGroupedExamples(this.tjsExamples, (ex) =>
410
- div(
411
- {
412
- class: 'nav-item',
413
- title: ex.description,
414
- onClick: () => this.selectTjsExample(ex),
415
- },
416
- ex.title || ex.name
417
- )
418
- )
419
- )
389
+ this.boundSection(
390
+ 'tjs-demos',
391
+ icons.code({ size: 16 }),
392
+ 'TJS Examples',
393
+ this.renderGroupedExamples(this.tjsExamples, (ex) => {
394
+ const name = ex.title || ex.name || 'Untitled'
395
+ return this.boundNavItem(name, ex, () => this.selectTjsExample(ex))
396
+ })
420
397
  ),
421
398
 
422
- // AJS Examples
423
- details(
424
- {
425
- open: this._openSection === 'ajs-demos',
426
- 'data-section': 'ajs-demos',
427
- onToggle: this.handleToggle,
428
- },
429
- summary(
430
- span({ class: 'section-icon' }, icons.code({ size: 16 })),
431
- 'AJS Examples'
432
- ),
433
- div(
434
- { class: 'section-content' },
435
- ...this.renderGroupedExamples(this.ajsExamples, (ex) =>
436
- div(
437
- {
438
- class: ex.requiresApi ? 'nav-item requires-api' : 'nav-item',
439
- title: ex.description,
440
- onClick: () => this.selectAjsExample(ex),
441
- },
442
- ex.title || ex.name
443
- )
444
- )
445
- )
399
+ this.boundSection(
400
+ 'ajs-demos',
401
+ icons.code({ size: 16 }),
402
+ 'AJS Examples',
403
+ this.renderGroupedExamples(this.ajsExamples, (ex) => {
404
+ const name = ex.title || ex.name || 'Untitled'
405
+ return this.boundNavItem(name, ex, () => this.selectAjsExample(ex))
406
+ })
446
407
  ),
447
408
 
448
- // TJS Docs
449
- details(
450
- {
451
- open: this._openSection === 'tjs-docs',
452
- 'data-section': 'tjs-docs',
453
- onToggle: this.handleToggle,
454
- },
455
- summary(
456
- span({ class: 'section-icon' }, icons.book({ size: 16 })),
457
- 'TJS Docs'
458
- ),
459
- div(
460
- { class: 'section-content' },
461
- ...this.getTjsDocs().map((doc) =>
462
- div(
463
- {
464
- class: 'nav-item',
465
- onClick: () => this.selectDoc(doc),
466
- },
467
- doc.title
468
- )
469
- )
409
+ this.boundSection(
410
+ 'tjs-docs',
411
+ icons.book({ size: 16 }),
412
+ 'TJS Docs',
413
+ this.getTjsDocs().map((doc) =>
414
+ this.boundNavItem(doc.title, doc, () => this.selectDoc(doc))
470
415
  )
471
416
  ),
472
417
 
473
- // AJS Docs
474
- details(
475
- {
476
- open: this._openSection === 'ajs-docs',
477
- 'data-section': 'ajs-docs',
478
- onToggle: this.handleToggle,
479
- },
480
- summary(
481
- span({ class: 'section-icon' }, icons.book({ size: 16 })),
482
- 'AJS Docs'
483
- ),
484
- div(
485
- { class: 'section-content' },
486
- ...this.getAjsDocs().map((doc) =>
487
- div(
488
- {
489
- class: 'nav-item',
490
- onClick: () => this.selectDoc(doc),
491
- },
492
- doc.title
493
- )
494
- )
418
+ this.boundSection(
419
+ 'ajs-docs',
420
+ icons.book({ size: 16 }),
421
+ 'AJS Docs',
422
+ this.getAjsDocs().map((doc) =>
423
+ this.boundNavItem(doc.title, doc, () => this.selectDoc(doc))
495
424
  )
496
425
  )
497
426
  )
498
427
  }
499
428
 
500
429
  handleToggle = (event: Event) => {
501
- const details = event.target as HTMLDetailsElement
502
- const section = details.getAttribute('data-section')
503
-
504
- if (details.open) {
505
- if (this._appState) this._appState.openSection.value = section
506
- // Close other sections (accordion behavior)
507
- this.querySelectorAll('details').forEach((d) => {
508
- if (d !== details && d.open) {
509
- d.open = false
510
- }
511
- })
430
+ const det = event.target as HTMLDetailsElement
431
+ const section = det.getAttribute('data-section')
432
+ if (!this._appState) return
433
+
434
+ const current = this._appState.openSection.valueOf()
435
+ if (det.open && current !== section) {
436
+ // User opened a section — update state (bind handles the rest)
437
+ this._appState.openSection.value = section
438
+ } else if (!det.open && current === section) {
439
+ // User closed the current section
440
+ this._appState.openSection.value = null
512
441
  }
513
442
  }
514
443
 
@@ -602,6 +531,7 @@ export class DemoNav extends Component {
602
531
  padding: '4px',
603
532
  border: 'none',
604
533
  background: 'transparent',
534
+ color: vars.textColor,
605
535
  cursor: 'pointer',
606
536
  },
607
537
  },
@@ -620,7 +550,8 @@ export class DemoNav extends Component {
620
550
  width: '500px',
621
551
  maxWidth: 'calc(100vw - 40px)',
622
552
  maxHeight: '80vh',
623
- background: 'white',
553
+ background: vars.background,
554
+ color: vars.textColor,
624
555
  borderRadius: '8px',
625
556
  boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
626
557
  overflow: 'hidden',
@@ -634,13 +565,16 @@ export class DemoNav extends Component {
634
565
  display: 'flex',
635
566
  alignItems: 'center',
636
567
  padding: '6px 12px',
637
- background: '#f3f4f6',
638
- borderBottom: '1px solid #e5e7eb',
568
+ background: vars.codeBackground,
569
+ borderBottom: `1px solid ${vars.codeBorder}`,
639
570
  cursor: 'move',
640
571
  },
641
572
  },
642
573
  span(
643
- { class: 'float-title', style: { flex: '1', fontWeight: '500' } },
574
+ {
575
+ class: 'float-title',
576
+ style: { flex: '1', fontWeight: '500', color: vars.textColor },
577
+ },
644
578
  doc.title
645
579
  ),
646
580
  closeBtn
@@ -20,19 +20,28 @@ const moduleCache = new Map<string, string>()
20
20
  // Common packages with pinned versions and ESM paths
21
21
  // Packages with proper "exports" or "module" fields in package.json
22
22
  // may work without explicit paths, but it's safer to specify them
23
- const PINNED_PACKAGES: Record<
24
- string,
25
- { version: string; path?: string; cdn?: string }
26
- > = {
23
+ interface PinnedPackage {
24
+ version: string
25
+ path?: string
26
+ cdn?: string
27
+ // Transitive deps to include in the import map when this package is imported
28
+ deps?: string[]
29
+ }
30
+
31
+ const PINNED_PACKAGES: Record<string, PinnedPackage> = {
27
32
  // tjs-lang itself (used by demos like Universal Endpoint)
28
33
  'tjs-lang': {
29
- version: '0.5.1',
30
- cdn: 'https://cdn.jsdelivr.net/npm/tjs-lang@0.5.1/dist/index.js',
34
+ version: '0.5.4',
35
+ cdn: 'https://cdn.jsdelivr.net/npm/tjs-lang@0.5.4/dist/index.js',
31
36
  },
32
37
 
33
38
  // tosijs ecosystem
34
39
  tosijs: { version: '1.2.0', path: '/dist/module.js' },
35
- 'tosijs-ui': { version: '1.2.0', path: '/dist/index.js' },
40
+ 'tosijs-ui': {
41
+ version: '1.2.0',
42
+ path: '/dist/index.js',
43
+ deps: ['tosijs', 'marked'],
44
+ },
36
45
 
37
46
  // Utilities - lodash-es is native ESM
38
47
  'lodash-es': { version: '4.17.21' },
@@ -56,13 +65,38 @@ const PINNED_PACKAGES: Record<
56
65
  'react-dom': {
57
66
  version: '18.2.0',
58
67
  cdn: 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm',
68
+ deps: ['react'],
59
69
  },
60
70
  'react-dom/client': {
61
71
  version: '18.2.0',
62
72
  cdn: 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/client/+esm',
73
+ deps: ['react', 'react-dom'],
63
74
  },
64
75
  }
65
76
 
77
+ /**
78
+ * Expand a list of specifiers to include transitive deps from PINNED_PACKAGES.
79
+ * Walks the deps graph to collect all needed packages.
80
+ */
81
+ function expandWithDeps(specifiers: string[]): string[] {
82
+ const all = new Set(specifiers)
83
+ const queue = [...specifiers]
84
+ while (queue.length > 0) {
85
+ const spec = queue.pop()!
86
+ const { name } = parseSpecifier(spec)
87
+ const pinned = PINNED_PACKAGES[spec] || PINNED_PACKAGES[name]
88
+ if (pinned?.deps) {
89
+ for (const dep of pinned.deps) {
90
+ if (!all.has(dep)) {
91
+ all.add(dep)
92
+ queue.push(dep)
93
+ }
94
+ }
95
+ }
96
+ }
97
+ return [...all]
98
+ }
99
+
66
100
  /**
67
101
  * Extract import specifiers from source code
68
102
  */
@@ -156,7 +190,7 @@ export function generateImportMap(specifiers: string[]): {
156
190
  } {
157
191
  const imports: Record<string, string> = {}
158
192
 
159
- for (const specifier of specifiers) {
193
+ for (const specifier of expandWithDeps(specifiers)) {
160
194
  imports[specifier] = getCDNUrl(specifier)
161
195
  }
162
196
 
@@ -300,7 +334,7 @@ export async function resolveImports(source: string): Promise<{
300
334
  errors: string[]
301
335
  localModules: string[]
302
336
  }> {
303
- const specifiers = extractImports(source)
337
+ const specifiers = expandWithDeps(extractImports(source))
304
338
  const errors: string[] = []
305
339
  const imports: Record<string, string> = {}
306
340
  const localModules: string[] = []