tjs-lang 0.2.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.
Files changed (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
@@ -0,0 +1,2097 @@
1
+ /**
2
+ * Demo Navigation Component
3
+ *
4
+ * Sidebar with 4 accordion details blocks:
5
+ * - AJS Examples (examples that open AJS playground)
6
+ * - TJS Examples (examples that open TJS playground)
7
+ * - AJS Docs (documentation that opens in floating viewer)
8
+ * - TJS Docs (documentation that opens in floating viewer)
9
+ */
10
+
11
+ import { Component, elements, ElementCreator, vars } from 'tosijs'
12
+ import {
13
+ xinFloat,
14
+ XinFloat,
15
+ markdownViewer,
16
+ MarkdownViewer,
17
+ icons,
18
+ } from 'tosijs-ui'
19
+ import { examples as ajsExamples } from './examples'
20
+ import { tsExamples, type TSExample } from './ts-examples'
21
+
22
+ const { div, details, summary, span, button } = elements
23
+
24
+ // TJS example interface
25
+ interface TjsExample {
26
+ name: string
27
+ description: string
28
+ code: string
29
+ group?: 'featured' | 'basics' | 'patterns' | 'fullstack' | 'advanced'
30
+ }
31
+
32
+ // TJS examples - demonstrating typed JavaScript features
33
+ export const tjsExamples: TjsExample[] = [
34
+ {
35
+ name: 'TJS Grammar Demo',
36
+ description: 'Comprehensive example exercising all TJS syntax features',
37
+ group: 'featured',
38
+ code: `/*#
39
+ # TJS Grammar Reference
40
+
41
+ This example exercises **every TJS feature**. Run it to see
42
+ tests pass and signature validation in action.
43
+
44
+ ## Parameter Syntax
45
+ | Syntax | Meaning |
46
+ |--------|---------|
47
+ | \`x: 0\` | Required number |
48
+ | \`x = 0\` | Optional, defaults to 0 |
49
+ | \`(? x: 0)\` | Force input validation |
50
+ | \`(! x: 0)\` | Skip input validation |
51
+
52
+ ## Return Type Syntax
53
+ | Syntax | Meaning |
54
+ |--------|---------|
55
+ | \`-> 10\` | Signature test runs at transpile |
56
+ | \`-? 10\` | + runtime output validation |
57
+ | \`-! 10\` | Skip signature test |
58
+ */
59
+
60
+ // ─────────────────────────────────────────────────────────
61
+ // SIGNATURE TESTS: -> runs at transpile time
62
+ // ─────────────────────────────────────────────────────────
63
+
64
+ /*#
65
+ Double a number. The \`-> 10\` means: double(5) must return 10.
66
+ This is verified when you save/transpile!
67
+ */
68
+ function double(x: 5) -> 10 {
69
+ return x * 2
70
+ }
71
+
72
+ /*#
73
+ Concatenate first and last name.
74
+ */
75
+ function fullName(first: 'Jane', last: 'Doe') -> 'Jane Doe' {
76
+ return first + ' ' + last
77
+ }
78
+
79
+ // ─────────────────────────────────────────────────────────
80
+ // SKIP SIGNATURE TEST: -! when return varies
81
+ // ─────────────────────────────────────────────────────────
82
+
83
+ /*#
84
+ Division with error handling. Uses \`-!\` because the error
85
+ path returns a different shape than success.
86
+ */
87
+ function divide(a: 10, b: 2) -! { ok: true, value: 5 } {
88
+ if (b === 0) {
89
+ return { ok: false, value: 0, error: 'div by zero' }
90
+ }
91
+ return { ok: true, value: a / b }
92
+ }
93
+
94
+ // ─────────────────────────────────────────────────────────
95
+ // EXPLICIT TESTS: test 'description' { }
96
+ // ─────────────────────────────────────────────────────────
97
+
98
+ test 'double works' {
99
+ expect(double(7)).toBe(14)
100
+ expect(double(0)).toBe(0)
101
+ }
102
+
103
+ test 'fullName concatenates' {
104
+ expect(fullName('John', 'Smith')).toBe('John Smith')
105
+ }
106
+
107
+ test 'divide handles zero' {
108
+ const result = divide(10, 0)
109
+ expect(result.ok).toBe(false)
110
+ }
111
+
112
+ test 'divide works normally' {
113
+ const result = divide(20, 4)
114
+ expect(result.ok).toBe(true)
115
+ expect(result.value).toBe(5)
116
+ }
117
+
118
+ // ─────────────────────────────────────────────────────────
119
+ // UNSAFE FUNCTIONS: (!) skips input validation
120
+ // ─────────────────────────────────────────────────────────
121
+
122
+ /*#
123
+ Fast path - no runtime type checks on inputs.
124
+ Use when you trust the caller (internal code).
125
+ */
126
+ function fastAdd(! a: 0, b: 0) -> 0 {
127
+ return a + b
128
+ }
129
+
130
+ // ─────────────────────────────────────────────────────────
131
+ // SAFE FUNCTIONS: (?) forces input validation
132
+ // ─────────────────────────────────────────────────────────
133
+
134
+ /*#
135
+ Critical path - always validate inputs even in unsafe blocks.
136
+ */
137
+ function safeAdd(? a: 0, b: 0) -> 0 {
138
+ return a + b
139
+ }
140
+
141
+ // ─────────────────────────────────────────────────────────
142
+ // COMPLEX TYPES
143
+ // ─────────────────────────────────────────────────────────
144
+
145
+ /*#
146
+ Object types are defined by example shape.
147
+ */
148
+ function createPoint(x: 3, y: 4) -> { x: 3, y: 4 } {
149
+ return { x, y }
150
+ }
151
+
152
+ /*#
153
+ Array types use single-element example.
154
+ */
155
+ function sum(nums: [1, 2, 3]) -> 6 {
156
+ return nums.reduce((a, b) => a + b, 0)
157
+ }
158
+
159
+ test 'createPoint returns structure' {
160
+ const p = createPoint(10, 20)
161
+ expect(p.x).toBe(10)
162
+ expect(p.y).toBe(20)
163
+ }
164
+
165
+ test 'sum adds array' {
166
+ expect(sum([1, 2, 3, 4])).toBe(10)
167
+ }
168
+
169
+ // ─────────────────────────────────────────────────────────
170
+ // OUTPUT
171
+ // ─────────────────────────────────────────────────────────
172
+
173
+ console.log('All signature tests passed at transpile time!')
174
+ console.log('double.__tjs:', double.__tjs)
175
+ console.log('Result:', double(21))
176
+ `,
177
+ },
178
+ {
179
+ name: 'Hello TJS',
180
+ description: 'Simple typed greeting function with docs and tests',
181
+ group: 'basics',
182
+ code: `/*#
183
+ The classic first function in any language.
184
+
185
+ Demonstrates:
186
+ - Type annotations via examples (\`name: 'World'\`)
187
+ - Return type example (\`-> 'Hello, World'\`) - tests the signature!
188
+ - Inline tests with \`test\` blocks
189
+ - Markdown documentation via \`/*#\` comments
190
+ */
191
+ test 'greet says hello' {
192
+ expect(greet('TJS')).toBe('Hello, TJS!')
193
+ }
194
+
195
+ function greet(name: 'World') -> 'Hello, World!' {
196
+ return \`Hello, \${name}!\`
197
+ }
198
+
199
+ // The type metadata includes the doc comment
200
+ console.log('Type info:', greet.__tjs)
201
+
202
+ // The ->! means: greet('World') MUST return 'Hello, World'
203
+ // This is verified at transpile time!
204
+ greet('TJS')`,
205
+ },
206
+ {
207
+ name: 'Required vs Optional',
208
+ description: 'Difference between : and = in parameters',
209
+ group: 'basics',
210
+ code: `/*#
211
+ ## Required vs Optional Parameters
212
+
213
+ In TJS, the punctuation tells you everything:
214
+
215
+ | Syntax | Meaning |
216
+ |--------|---------|
217
+ | \`param: 'value'\` | **Required** - must be provided |
218
+ | \`param = 'value'\` | **Optional** - defaults to value |
219
+
220
+ The example value after \`:\` or \`=\` defines the type.
221
+ */
222
+ test 'requires name and email' {
223
+ const user = createUser('Alice', 'alice@test.com')
224
+ expect(user.name).toBe('Alice')
225
+ expect(user.age).toBe(0) // default
226
+ }
227
+
228
+ function createUser(
229
+ name: 'anonymous',
230
+ email: 'user@example.com',
231
+ age = 0,
232
+ admin = false
233
+ ) -> { name: '', email: '', age: 0, admin: false } {
234
+ return { name, email, age, admin }
235
+ }
236
+
237
+ // Check the metadata
238
+ console.log('Params:', createUser.__tjs.params)
239
+ createUser('Alice', 'alice@example.com')`,
240
+ },
241
+ {
242
+ name: 'Object Types',
243
+ description: 'Typed object parameters and returns',
244
+ group: 'basics',
245
+ code: `/*#
246
+ ## Object Types
247
+
248
+ Object shapes are defined by example:
249
+ \`{ first: '', last: '' }\` means an object with string properties.
250
+
251
+ The return type \`-> { x: 0, y: 0 }\` is tested at transpile time!
252
+ */
253
+ test 'createPoint returns correct structure' {
254
+ const p = createPoint(5, 10)
255
+ expect(p.x).toBe(5)
256
+ expect(p.y).toBe(10)
257
+ }
258
+
259
+ function getFullName(person: { first: '', last: '' }) -> 'Jane Doe' {
260
+ return person.first + ' ' + person.last
261
+ }
262
+
263
+ function createPoint(x: 0, y: 0) -> { x: 0, y: 0 } {
264
+ return { x, y }
265
+ }
266
+
267
+ function distance(p1: { x: 0, y: 0 }, p2: { x: 0, y: 0 }) -> 5 {
268
+ const dx = p2.x - p1.x
269
+ const dy = p2.y - p1.y
270
+ return Math.sqrt(dx * dx + dy * dy)
271
+ }
272
+
273
+ // Usage - signature tests verify these at transpile time
274
+ const name = getFullName({ first: 'Jane', last: 'Doe' }) // -> 'Jane Doe'
275
+ const dist = distance({ x: 0, y: 0 }, { x: 3, y: 4 }) // -> 5
276
+
277
+ console.log('Name:', name)
278
+ console.log('Distance:', dist)`,
279
+ },
280
+ {
281
+ name: 'Array Types',
282
+ description: 'Working with typed arrays',
283
+ group: 'basics',
284
+ code: `/*#
285
+ ## Array Types
286
+
287
+ Array types use a single-element example:
288
+ - \`[0]\` = array of numbers
289
+ - \`['']\` = array of strings
290
+ - \`[{ x: 0 }]\` = array of objects with shape { x: number }
291
+ */
292
+ test 'sum adds numbers' {
293
+ expect(sum([1, 2, 3, 4])).toBe(10)
294
+ }
295
+
296
+ test 'stats calculates correctly' {
297
+ const s = stats([10, 20, 30])
298
+ expect(s.min).toBe(10)
299
+ expect(s.max).toBe(30)
300
+ expect(s.avg).toBe(20)
301
+ }
302
+
303
+ function sum(numbers: [0]) -> 10 {
304
+ return numbers.reduce((a, b) => a + b, 0)
305
+ }
306
+
307
+ function average(numbers: [0]) -> 20 {
308
+ if (numbers.length === 0) return 0
309
+ return sum(numbers) / numbers.length
310
+ }
311
+
312
+ function stats(data: [0]) -> { min: 10, max: 30, avg: 20 } {
313
+ if (data.length === 0) {
314
+ return { min: 0, max: 0, avg: 0 }
315
+ }
316
+ return {
317
+ min: Math.min(...data),
318
+ max: Math.max(...data),
319
+ avg: average(data)
320
+ }
321
+ }
322
+
323
+ // Signature test: stats([10, 20, 30]) -> { min: 10, max: 30, avg: 20 }
324
+ stats([10, 20, 30])`,
325
+ },
326
+ {
327
+ name: 'Higher-Order Functions',
328
+ description: 'Functions that take or return functions',
329
+ group: 'patterns',
330
+ code: `// TJS handles higher-order functions
331
+ // Note: Function type annotations use simple syntax
332
+
333
+ function mapStrings(arr: [''], fn = (x) => x) -> [''] {
334
+ return arr.map(fn)
335
+ }
336
+
337
+ function filterNumbers(arr: [0], predicate = (x) => true) -> [0] {
338
+ return arr.filter(predicate)
339
+ }
340
+
341
+ function compose(f = (x) => x, g = (x) => x) -> 0 {
342
+ // Returns a composed function, demo returns result
343
+ const composed = (x) => f(g(x))
344
+ return composed(5)
345
+ }
346
+
347
+ // Usage examples
348
+ const double = (x) => x * 2
349
+ const addOne = (x) => x + 1
350
+
351
+ // Map strings to uppercase
352
+ const words = mapStrings(['hello', 'world'], s => s.toUpperCase())
353
+
354
+ // Filter even numbers
355
+ const evens = filterNumbers([1, 2, 3, 4, 5, 6], x => x % 2 === 0)
356
+
357
+ // Compose functions: (5 * 2) + 1 = 11
358
+ const result = compose(addOne, double)
359
+
360
+ console.log('Mapped:', words)
361
+ console.log('Filtered:', evens)
362
+ console.log('Composed result:', result)
363
+
364
+ result`,
365
+ },
366
+ {
367
+ name: 'Async Functions',
368
+ description: 'Typed async/await patterns',
369
+ group: 'patterns',
370
+ code: `// Async functions work naturally
371
+
372
+ async function fetchUser(id: 'user-1') -> { name: '', email: '' } {
373
+ // Simulated API call
374
+ await new Promise(resolve => setTimeout(resolve, 100))
375
+ return {
376
+ name: 'User ' + id,
377
+ email: id + '@example.com'
378
+ }
379
+ }
380
+
381
+ async function fetchUsers(ids: ['']) -> [{ name: '', email: '' }] {
382
+ return Promise.all(ids.map(id => fetchUser(id)))
383
+ }
384
+
385
+ // Run it
386
+ await fetchUsers(['alice', 'bob', 'charlie'])`,
387
+ },
388
+ {
389
+ name: 'Error Handling',
390
+ description: 'Type-safe error handling patterns',
391
+ group: 'patterns',
392
+ code: `/*#
393
+ ## Monadic Error Handling
394
+
395
+ TJS uses the Result pattern - errors are values, not exceptions.
396
+ This makes error handling explicit and type-safe.
397
+
398
+ Note: Using \`-!\` to skip signature test since error paths
399
+ return different shapes.
400
+ */
401
+ test 'divide handles zero' {
402
+ const result = divide(10, 0)
403
+ expect(result.ok).toBe(false)
404
+ expect(result.error).toBe('Division by zero')
405
+ }
406
+
407
+ test 'divide works normally' {
408
+ const result = divide(10, 2)
409
+ expect(result.ok).toBe(true)
410
+ expect(result.value).toBe(5)
411
+ }
412
+
413
+ function divide(a: 10, b: 2) -! { ok: true, value: 5, error: '' } {
414
+ if (b === 0) {
415
+ return { ok: false, value: 0, error: 'Division by zero' }
416
+ }
417
+ return { ok: true, value: a / b, error: '' }
418
+ }
419
+
420
+ function safeParse(json: '{"x":1}') -! { ok: true, data: null, error: '' } {
421
+ try {
422
+ return { ok: true, data: JSON.parse(json), error: '' }
423
+ } catch (e) {
424
+ return { ok: false, data: null, error: e.message }
425
+ }
426
+ }
427
+
428
+ // Usage - errors are values you can inspect
429
+ const result = divide(10, 0)
430
+ if (result.ok) {
431
+ console.log('Result:', result.value)
432
+ } else {
433
+ console.log('Error:', result.error)
434
+ }`,
435
+ },
436
+ {
437
+ name: 'Schema Validation',
438
+ description: 'Using Schema for runtime type checking',
439
+ group: 'patterns',
440
+ code: `// TJS integrates with Schema for validation
441
+ import { Schema } from 'tosijs-schema'
442
+
443
+ // Define a schema
444
+ const UserSchema = Schema({
445
+ name: 'anonymous',
446
+ email: 'user@example.com',
447
+ age: 0
448
+ })
449
+
450
+ // Validate data
451
+ function validateUser(data: { name: '', email: '', age: 0 }) -> { valid: true, errors: [''] } {
452
+ const errors = []
453
+
454
+ if (!UserSchema.validate(data)) {
455
+ errors.push('Invalid user structure')
456
+ }
457
+
458
+ return {
459
+ valid: errors.length === 0,
460
+ errors
461
+ }
462
+ }
463
+
464
+ validateUser({ name: 'Alice', email: 'alice@test.com', age: 30 })`,
465
+ },
466
+ {
467
+ name: 'Date Formatting (with import)',
468
+ description: 'Uses date-fns for date formatting via ESM import',
469
+ group: 'patterns',
470
+ code: `/**
471
+ * # Date Formatting with Imports
472
+ *
473
+ * This example demonstrates importing an external ESM module
474
+ * (date-fns) and using it with TJS type safety.
475
+ */
476
+
477
+ import { format, formatDistance, addDays, parseISO } from 'date-fns'
478
+
479
+ // Format a date with various patterns
480
+ function formatDate(date: '2024-01-15', pattern: 'yyyy-MM-dd') -> '' {
481
+ const parsed = parseISO(date)
482
+ return format(parsed, pattern)
483
+ }
484
+
485
+ // Get human-readable relative time
486
+ function timeAgo(date: '2024-01-15') -> '' {
487
+ const parsed = parseISO(date)
488
+ return formatDistance(parsed, new Date(), { addSuffix: true })
489
+ }
490
+
491
+ // Add days to a date
492
+ function addWorkdays(date: '2024-01-15', days: 5) -> '' {
493
+ const parsed = parseISO(date)
494
+ const result = addDays(parsed, days)
495
+ return format(result, 'yyyy-MM-dd')
496
+ }
497
+
498
+ // Complex date operation with validation
499
+ function createEvent(input: {
500
+ title: 'Meeting',
501
+ startDate: '2024-01-15',
502
+ durationDays: 1
503
+ }) -> { title: '', start: '', end: '', formatted: '' } {
504
+ const start = parseISO(input.startDate)
505
+ const end = addDays(start, input.durationDays)
506
+
507
+ return {
508
+ title: input.title,
509
+ start: format(start, 'yyyy-MM-dd'),
510
+ end: format(end, 'yyyy-MM-dd'),
511
+ formatted: \`\${input.title}: \${format(start, 'MMM d')} - \${format(end, 'MMM d, yyyy')}\`
512
+ }
513
+ }
514
+
515
+ test('formatDate works with different patterns') {
516
+ expect(formatDate('2024-01-15', 'yyyy-MM-dd')).toBe('2024-01-15')
517
+ expect(formatDate('2024-01-15', 'MMMM d, yyyy')).toBe('January 15, 2024')
518
+ expect(formatDate('2024-01-15', 'EEE')).toBe('Mon')
519
+ }
520
+
521
+ test('addWorkdays calculates correctly') {
522
+ expect(addWorkdays('2024-01-15', 5)).toBe('2024-01-20')
523
+ expect(addWorkdays('2024-01-15', 0)).toBe('2024-01-15')
524
+ }
525
+
526
+ test('createEvent formats event correctly') {
527
+ const event = createEvent({
528
+ title: 'Conference',
529
+ startDate: '2024-06-10',
530
+ durationDays: 3
531
+ })
532
+ expect(event.title).toBe('Conference')
533
+ expect(event.start).toBe('2024-06-10')
534
+ expect(event.end).toBe('2024-06-13')
535
+ }
536
+
537
+ // Run example
538
+ console.log('Format examples:')
539
+ console.log(' ISO:', formatDate('2024-01-15', 'yyyy-MM-dd'))
540
+ console.log(' Long:', formatDate('2024-01-15', 'MMMM d, yyyy'))
541
+ console.log(' Day:', formatDate('2024-01-15', 'EEEE'))
542
+
543
+ console.log('\\nRelative time:', timeAgo('2024-01-01'))
544
+
545
+ const event = createEvent({
546
+ title: 'Launch Party',
547
+ startDate: '2024-03-15',
548
+ durationDays: 2
549
+ })
550
+ console.log('\\nEvent:', event.formatted)`,
551
+ },
552
+ {
553
+ name: 'Local Module Imports',
554
+ description: 'Import from modules you save in the playground',
555
+ group: 'patterns',
556
+ code: `/*#
557
+ # Local Module Imports
558
+
559
+ You can import from modules saved in the playground!
560
+
561
+ ## How it works:
562
+ 1. Save a module (use the Save button, give it a name like "math")
563
+ 2. Import it by name from another file
564
+
565
+ ## Try it:
566
+ 1. First, create and save a module named "mymath":
567
+
568
+ \`\`\`javascript
569
+ export function add(a: 0, b: 0) -> 0 {
570
+ return a + b
571
+ }
572
+
573
+ export function multiply(a: 0, b: 0) -> 0 {
574
+ return a * b
575
+ }
576
+ \`\`\`
577
+
578
+ 2. Then run this code (it imports from your saved module)
579
+ */
580
+
581
+ // This imports from a module you saved in the playground
582
+ // Change 'mymath' to match whatever name you used when saving
583
+ import { add, multiply } from 'mymath'
584
+
585
+ function calculate(x: 0, y: 0) -> 0 {
586
+ // (x + y) * 2
587
+ return multiply(add(x, y), 2)
588
+ }
589
+
590
+ test 'calculate combines add and multiply' {
591
+ expect(calculate(3, 4)).toBe(14) // (3 + 4) * 2 = 14
592
+ }
593
+
594
+ console.log('calculate(3, 4) =', calculate(3, 4))
595
+ console.log('calculate(10, 5) =', calculate(10, 5))`,
596
+ },
597
+ {
598
+ name: 'Lodash Utilities (with import)',
599
+ description: 'Uses lodash-es for utility functions via ESM import',
600
+ group: 'patterns',
601
+ code: `/**
602
+ * # Lodash Utilities with Type Safety
603
+ *
604
+ * Demonstrates using lodash-es with TJS runtime validation.
605
+ */
606
+
607
+ import { groupBy, sortBy, uniqBy, debounce, chunk } from 'lodash-es'
608
+
609
+ // Group items by a key
610
+ function groupUsers(users: [{ name: '', dept: '' }], key: 'dept')
611
+ -> { [key: '']: [{ name: '', dept: '' }] } {
612
+ return groupBy(users, key)
613
+ }
614
+
615
+ // Sort items by property
616
+ function sortByAge(users: [{ name: '', age: 0 }]) -> [{ name: '', age: 0 }] {
617
+ return sortBy(users, ['age'])
618
+ }
619
+
620
+ // Remove duplicates by property
621
+ function uniqueByEmail(users: [{ email: '', name: '' }]) -> [{ email: '', name: '' }] {
622
+ return uniqBy(users, 'email')
623
+ }
624
+
625
+ // Chunk array into smaller arrays
626
+ // Note: nested array types like [['']] aren't supported yet, so we omit return type
627
+ function paginate(items: [''], pageSize: 10) {
628
+ return chunk(items, pageSize)
629
+ }
630
+
631
+ // Process data pipeline
632
+ function processUserData(input: {
633
+ users: [{ id: 0, name: '', email: '', dept: '' }]
634
+ }) -> {
635
+ byDept: { [key: '']: [{ id: 0, name: '', email: '', dept: '' }] },
636
+ unique: [{ id: 0, name: '', email: '', dept: '' }],
637
+ count: 0
638
+ } {
639
+ const unique = uniqBy(input.users, 'email')
640
+ const byDept = groupBy(unique, 'dept')
641
+
642
+ return {
643
+ byDept,
644
+ unique: sortBy(unique, ['name']),
645
+ count: unique.length
646
+ }
647
+ }
648
+
649
+ test('groupUsers groups by department') {
650
+ const users = [
651
+ { name: 'Alice', dept: 'eng' },
652
+ { name: 'Bob', dept: 'sales' },
653
+ { name: 'Carol', dept: 'eng' }
654
+ ]
655
+ const grouped = groupUsers(users, 'dept')
656
+ expect(grouped.eng.length).toBe(2)
657
+ expect(grouped.sales.length).toBe(1)
658
+ }
659
+
660
+ test('sortByAge sorts correctly') {
661
+ const users = [
662
+ { name: 'Alice', age: 30 },
663
+ { name: 'Bob', age: 25 },
664
+ { name: 'Carol', age: 35 }
665
+ ]
666
+ const sorted = sortByAge(users)
667
+ expect(sorted[0].name).toBe('Bob')
668
+ expect(sorted[2].name).toBe('Carol')
669
+ }
670
+
671
+ test('uniqueByEmail deduplicates') {
672
+ const users = [
673
+ { email: 'a@b.com', name: 'Alice' },
674
+ { email: 'a@b.com', name: 'Alice2' },
675
+ { email: 'c@d.com', name: 'Carol' }
676
+ ]
677
+ const unique = uniqueByEmail(users)
678
+ expect(unique.length).toBe(2)
679
+ }
680
+
681
+ test('paginate chunks correctly') {
682
+ const items = ['a', 'b', 'c', 'd', 'e']
683
+ const pages = paginate(items, 2)
684
+ expect(pages.length).toBe(3)
685
+ expect(pages[0]).toEqual(['a', 'b'])
686
+ expect(pages[2]).toEqual(['e'])
687
+ }
688
+
689
+ // Run example
690
+ const users = [
691
+ { id: 1, name: 'Alice', email: 'alice@co.com', dept: 'Engineering' },
692
+ { id: 2, name: 'Bob', email: 'bob@co.com', dept: 'Sales' },
693
+ { id: 3, name: 'Carol', email: 'carol@co.com', dept: 'Engineering' },
694
+ { id: 4, name: 'Dave', email: 'alice@co.com', dept: 'Marketing' }, // dupe email
695
+ ]
696
+
697
+ const result = processUserData({ users })
698
+ console.log('Unique users:', result.count)
699
+ console.log('Departments:', Object.keys(result.byDept))
700
+ console.log('Engineering team:', result.byDept['Engineering']?.map(u => u.name))`,
701
+ },
702
+ {
703
+ name: 'Full-Stack Demo: User Service',
704
+ description:
705
+ 'A complete backend service with typed endpoints - save this first!',
706
+ group: 'fullstack',
707
+ code: `/**
708
+ * # User Service
709
+ *
710
+ * A complete backend service running in the browser.
711
+ * Save this module as "user-service", then run the client example.
712
+ *
713
+ * Features:
714
+ * - Type-safe endpoints with validation
715
+ * - In-memory data store
716
+ * - Full CRUD operations
717
+ */
718
+
719
+ // In-memory store (would be a real DB in production)
720
+ const users = new Map()
721
+ let nextId = 1
722
+
723
+ // Create a new user
724
+ export function createUser(input: {
725
+ name: 'Alice',
726
+ email: 'alice@example.com'
727
+ }) -> { id: 0, name: '', email: '', createdAt: '' } {
728
+ const user = {
729
+ id: nextId++,
730
+ name: input.name,
731
+ email: input.email,
732
+ createdAt: new Date().toISOString()
733
+ }
734
+ users.set(user.id, user)
735
+ return user
736
+ }
737
+
738
+ // Get user by ID (returns empty object if not found - union types not yet supported)
739
+ export function getUser(input: { id: 1 }) -> { id: 0, name: '', email: '', createdAt: '' } {
740
+ return users.get(input.id) || { id: 0, name: '', email: '', createdAt: '' }
741
+ }
742
+
743
+ // Update a user (returns empty object if not found - union types not yet supported)
744
+ export function updateUser(input: {
745
+ id: 1,
746
+ name: 'Alice',
747
+ email: 'alice@example.com'
748
+ }) -> { id: 0, name: '', email: '', createdAt: '' } {
749
+ const existing = users.get(input.id)
750
+ if (!existing) return { id: 0, name: '', email: '', createdAt: '' }
751
+
752
+ const updated = { ...existing, name: input.name, email: input.email }
753
+ users.set(input.id, updated)
754
+ return updated
755
+ }
756
+
757
+ // Delete a user
758
+ export function deleteUser(input: { id: 1 }) -> { success: true, deleted: 0 } {
759
+ const existed = users.has(input.id)
760
+ users.delete(input.id)
761
+ return { success: existed, deleted: existed ? input.id : 0 }
762
+ }
763
+
764
+ // List all users
765
+ export function listUsers(input: { limit: 10, offset: 0 })
766
+ -> { users: [{ id: 0, name: '', email: '', createdAt: '' }], total: 0 } {
767
+ const all = [...users.values()]
768
+ const slice = all.slice(input.offset, input.offset + input.limit)
769
+ return { users: slice, total: all.length }
770
+ }
771
+
772
+ // Search users by name
773
+ export function searchUsers(input: { query: '' })
774
+ -> { users: [{ id: 0, name: '', email: '', createdAt: '' }] } {
775
+ const query = input.query.toLowerCase()
776
+ const matches = [...users.values()].filter(u =>
777
+ u.name.toLowerCase().includes(query)
778
+ )
779
+ return { users: matches }
780
+ }
781
+
782
+ // Test the service
783
+ test('createUser creates user with ID') {
784
+ const user = createUser({ name: 'Test', email: 'test@test.com' })
785
+ expect(user.id).toBeGreaterThan(0)
786
+ expect(user.name).toBe('Test')
787
+ }
788
+
789
+ test('getUser returns created user') {
790
+ const created = createUser({ name: 'Bob', email: 'bob@test.com' })
791
+ const fetched = getUser({ id: created.id })
792
+ expect(fetched?.name).toBe('Bob')
793
+ }
794
+
795
+ test('updateUser modifies user') {
796
+ const user = createUser({ name: 'Original', email: 'orig@test.com' })
797
+ const updated = updateUser({ id: user.id, name: 'Updated', email: 'new@test.com' })
798
+ expect(updated?.name).toBe('Updated')
799
+ }
800
+
801
+ test('deleteUser removes user') {
802
+ const user = createUser({ name: 'ToDelete', email: 'del@test.com' })
803
+ const result = deleteUser({ id: user.id })
804
+ expect(result.success).toBe(true)
805
+ expect(getUser({ id: user.id })).toBe(null)
806
+ }
807
+
808
+ // Demo
809
+ console.log('=== User Service Demo ===\\n')
810
+
811
+ const alice = createUser({ name: 'Alice', email: 'alice@company.com' })
812
+ console.log('Created:', alice)
813
+
814
+ const bob = createUser({ name: 'Bob', email: 'bob@company.com' })
815
+ console.log('Created:', bob)
816
+
817
+ const carol = createUser({ name: 'Carol', email: 'carol@company.com' })
818
+ console.log('Created:', carol)
819
+
820
+ console.log('\\nAll users:', listUsers({ limit: 10, offset: 0 }))
821
+ console.log('\\nSearch "ob":', searchUsers({ query: 'ob' }))
822
+
823
+ // Type validation in action
824
+ console.log('\\nType validation test:')
825
+ const badResult = createUser({ name: 123 }) // Wrong type
826
+ console.log('Bad input result:', badResult) // Returns $error object`,
827
+ },
828
+ {
829
+ name: 'Full-Stack Demo: Client App',
830
+ description:
831
+ 'Frontend that calls the User Service - run after saving user-service!',
832
+ group: 'fullstack',
833
+ code: `/**
834
+ * # Client Application
835
+ *
836
+ * A frontend that calls the User Service.
837
+ *
838
+ * **First:** Run the "User Service" example and save it as "user-service"
839
+ * **Then:** Run this client to see full-stack in action
840
+ *
841
+ * This demonstrates:
842
+ * - Importing local TJS modules
843
+ * - Type-safe service calls
844
+ * - Error handling
845
+ */
846
+
847
+ // Import from local module (saved in playground)
848
+ import { createUser, getUser, listUsers, searchUsers } from 'user-service'
849
+
850
+ // Helper to display results
851
+ function display(label: '', data: {}) {
852
+ console.log(\`\\n\${label}:\`)
853
+ console.log(JSON.stringify(data, null, 2))
854
+ }
855
+
856
+ // Main app
857
+ async function main() {
858
+ console.log('=== Client App ===')
859
+ console.log('Connecting to user-service...\\n')
860
+
861
+ // Create some users
862
+ const user1 = createUser({ name: 'Dave', email: 'dave@startup.io' })
863
+ display('Created user', user1)
864
+
865
+ const user2 = createUser({ name: 'Eve', email: 'eve@startup.io' })
866
+ display('Created user', user2)
867
+
868
+ // Fetch a user
869
+ const fetched = getUser({ id: user1.id })
870
+ display('Fetched user', fetched)
871
+
872
+ // List all
873
+ const all = listUsers({ limit: 100, offset: 0 })
874
+ display('All users', all)
875
+
876
+ // Search
877
+ const results = searchUsers({ query: 'eve' })
878
+ display('Search results for "eve"', results)
879
+
880
+ // Type error handling
881
+ console.log('\\n--- Type Validation Demo ---')
882
+ const badResult = createUser({ name: 999 })
883
+ if (badResult.$error) {
884
+ console.log('Caught type error:', badResult.message)
885
+ }
886
+
887
+ console.log('\\n=== Full-Stack Demo Complete ===')
888
+ console.log('Everything ran in the browser. No server. No build step.')
889
+ }
890
+
891
+ main()`,
892
+ },
893
+ {
894
+ name: 'Full-Stack Demo: Todo API',
895
+ description: 'Complete REST-style Todo API with persistence',
896
+ group: 'fullstack',
897
+ code: `/**
898
+ * # Todo API Service
899
+ *
900
+ * A REST-style API for todo management.
901
+ * Demonstrates a more complete service pattern.
902
+ */
903
+
904
+ // Simulated persistence layer
905
+ const todos = new Map()
906
+ let nextId = 1
907
+
908
+ // Types
909
+ type Todo = { id: number, title: string, completed: boolean, createdAt: string }
910
+ type CreateInput = { title: 'Buy milk' }
911
+ type UpdateInput = { id: 1, title: 'Buy milk', completed: false }
912
+ type FilterInput = { completed: true } | { completed: false } | {}
913
+
914
+ // POST /todos - Create
915
+ export function createTodo(input: { title: 'New todo' })
916
+ -> { id: 0, title: '', completed: false, createdAt: '' } {
917
+ const todo = {
918
+ id: nextId++,
919
+ title: input.title,
920
+ completed: false,
921
+ createdAt: new Date().toISOString()
922
+ }
923
+ todos.set(todo.id, todo)
924
+ return todo
925
+ }
926
+
927
+ // GET /todos/:id - Read one (returns empty if not found)
928
+ export function getTodo(input: { id: 1 })
929
+ -> { id: 0, title: '', completed: false, createdAt: '' } {
930
+ return todos.get(input.id) || { id: 0, title: '', completed: false, createdAt: '' }
931
+ }
932
+
933
+ // GET /todos - Read all (with optional filter)
934
+ export function listTodos(input: { completed: true } | {})
935
+ -> { todos: [{ id: 0, title: '', completed: false, createdAt: '' }] } {
936
+ let items = [...todos.values()]
937
+
938
+ if ('completed' in input) {
939
+ items = items.filter(t => t.completed === input.completed)
940
+ }
941
+
942
+ return { todos: items }
943
+ }
944
+
945
+ // PUT /todos/:id - Update (returns empty if not found)
946
+ export function updateTodo(input: { id: 1, title: '', completed: false })
947
+ -> { id: 0, title: '', completed: false, createdAt: '' } {
948
+ const existing = todos.get(input.id)
949
+ if (!existing) return { id: 0, title: '', completed: false, createdAt: '' }
950
+
951
+ const updated = {
952
+ ...existing,
953
+ title: input.title ?? existing.title,
954
+ completed: input.completed ?? existing.completed
955
+ }
956
+ todos.set(input.id, updated)
957
+ return updated
958
+ }
959
+
960
+ // DELETE /todos/:id - Delete
961
+ export function deleteTodo(input: { id: 1 }) -> { deleted: true } {
962
+ const existed = todos.has(input.id)
963
+ todos.delete(input.id)
964
+ return { deleted: existed }
965
+ }
966
+
967
+ // PATCH /todos/:id/toggle - Toggle completion (returns empty if not found)
968
+ export function toggleTodo(input: { id: 1 })
969
+ -> { id: 0, title: '', completed: false, createdAt: '' } {
970
+ const todo = todos.get(input.id)
971
+ if (!todo) return { id: 0, title: '', completed: false, createdAt: '' }
972
+
973
+ todo.completed = !todo.completed
974
+ return todo
975
+ }
976
+
977
+ // DELETE /todos/completed - Clear completed
978
+ export function clearCompleted(input: {}) -> { cleared: 0 } {
979
+ let cleared = 0
980
+ for (const [id, todo] of todos) {
981
+ if (todo.completed) {
982
+ todos.delete(id)
983
+ cleared++
984
+ }
985
+ }
986
+ return { cleared }
987
+ }
988
+
989
+ // Tests
990
+ test('CRUD operations work') {
991
+ const todo = createTodo({ title: 'Test todo' })
992
+ expect(todo.id).toBeGreaterThan(0)
993
+ expect(todo.completed).toBe(false)
994
+
995
+ const fetched = getTodo({ id: todo.id })
996
+ expect(fetched?.title).toBe('Test todo')
997
+
998
+ const toggled = toggleTodo({ id: todo.id })
999
+ expect(toggled?.completed).toBe(true)
1000
+
1001
+ const deleted = deleteTodo({ id: todo.id })
1002
+ expect(deleted.deleted).toBe(true)
1003
+ }
1004
+
1005
+ // Demo
1006
+ console.log('=== Todo API Demo ===\\n')
1007
+
1008
+ // Create todos
1009
+ createTodo({ title: 'Learn TJS' })
1010
+ createTodo({ title: 'Build something cool' })
1011
+ createTodo({ title: 'Ship it' })
1012
+
1013
+ console.log('Created 3 todos')
1014
+ console.log('All:', listTodos({}))
1015
+
1016
+ // Complete first one
1017
+ const first = listTodos({}).todos[0]
1018
+ toggleTodo({ id: first.id })
1019
+ console.log('\\nToggled first todo')
1020
+ console.log('Completed:', listTodos({ completed: true }))
1021
+ console.log('Pending:', listTodos({ completed: false }))
1022
+
1023
+ // Clear completed
1024
+ console.log('\\nClearing completed...')
1025
+ console.log(clearCompleted({}))
1026
+ console.log('Remaining:', listTodos({}))`,
1027
+ },
1028
+ {
1029
+ name: 'The Universal Endpoint',
1030
+ description:
1031
+ 'One endpoint. Any logic. Zero deployment. This is the whole thing.',
1032
+ group: 'advanced',
1033
+ code: `/**
1034
+ * # The Universal Endpoint
1035
+ *
1036
+ * This is the entire backend industry in 50 lines.
1037
+ *
1038
+ * What this replaces:
1039
+ * - GraphQL servers
1040
+ * - REST API forests
1041
+ * - Firebase/Lambda/Vercel Functions
1042
+ * - Kubernetes deployments
1043
+ * - The backend priesthood
1044
+ *
1045
+ * How it works:
1046
+ * 1. Client sends logic (not just data)
1047
+ * 2. Server executes it with bounded resources
1048
+ * 3. That's it. That's the whole thing.
1049
+ */
1050
+
1051
+ import { AgentVM, ajs, coreAtoms } from 'tjs-lang'
1052
+
1053
+ // ============================================================
1054
+ // THE UNIVERSAL ENDPOINT (This is the entire backend)
1055
+ // ============================================================
1056
+
1057
+ export async function post(req: {
1058
+ body: {
1059
+ agent: '', // The logic to execute (AJS source)
1060
+ args: {}, // Input data
1061
+ fuel: 1000 // Max compute units (like gas)
1062
+ },
1063
+ headers: { authorization: '' }
1064
+ }) -> { result: {}, fuelUsed: 0, status: '' } | { error: '', fuelUsed: 0 } {
1065
+
1066
+ // 1. Parse the agent (it's just code as data)
1067
+ let ast
1068
+ try {
1069
+ ast = ajs(req.body.agent)
1070
+ } catch (e) {
1071
+ return { error: \`Parse error: \${e.message}\`, fuelUsed: 0 }
1072
+ }
1073
+
1074
+ // 2. Create VM with capabilities (this is what you monetize)
1075
+ const vm = new AgentVM({
1076
+ // Your database, your auth, your AI - exposed as capabilities
1077
+ // Agents can only do what you allow
1078
+ })
1079
+
1080
+ // 3. Execute with bounded resources
1081
+ const result = await vm.run(ast, req.body.args, {
1082
+ fuel: Math.min(req.body.fuel, 10000), // Cap fuel
1083
+ timeoutMs: 5000 // Cap time
1084
+ })
1085
+
1086
+ // 4. Return result (or error - but we didn't crash)
1087
+ if (result.error) {
1088
+ return {
1089
+ error: result.error.message,
1090
+ fuelUsed: result.fuelUsed,
1091
+ }
1092
+ }
1093
+
1094
+ return {
1095
+ result: result.result,
1096
+ fuelUsed: result.fuelUsed,
1097
+ status: 'success'
1098
+ }
1099
+ }
1100
+
1101
+ // ============================================================
1102
+ // DEMO: Let's use it
1103
+ // ============================================================
1104
+
1105
+ // Simulate the endpoint
1106
+ const endpoint = post
1107
+
1108
+ // --- TEST 1: Simple computation (Success) ---
1109
+ console.log('═══════════════════════════════════════════')
1110
+ console.log('TEST 1: Simple Agent')
1111
+ console.log('═══════════════════════════════════════════')
1112
+
1113
+ const simpleAgent = \`
1114
+ function compute({ x, y }) {
1115
+ let sum = x + y
1116
+ let product = x * y
1117
+ return { sum, product, message: 'Math is easy' }
1118
+ }
1119
+ \`
1120
+
1121
+ const result1 = await endpoint({
1122
+ body: { agent: simpleAgent, args: { x: 7, y: 6 }, fuel: 100 },
1123
+ headers: { authorization: 'token_123' }
1124
+ })
1125
+
1126
+ console.log('Agent: compute({ x: 7, y: 6 })')
1127
+ console.log('Result:', result1.result)
1128
+ console.log('Fuel used:', result1.fuelUsed)
1129
+ console.log('Status: ✓ Success')
1130
+
1131
+
1132
+ // --- TEST 2: Infinite loop (Fuel Exhausted) ---
1133
+ console.log('\\n═══════════════════════════════════════════')
1134
+ console.log('TEST 2: Malicious Agent (Infinite Loop)')
1135
+ console.log('═══════════════════════════════════════════')
1136
+
1137
+ const maliciousAgent = \`
1138
+ function attack({ }) {
1139
+ let i = 0
1140
+ while (true) {
1141
+ i = i + 1
1142
+ // This would hang your Express server forever
1143
+ // This would cost you $10,000 on Lambda
1144
+ // This would crash your Kubernetes pod
1145
+ }
1146
+ return { i }
1147
+ }
1148
+ \`
1149
+
1150
+ const result2 = await endpoint({
1151
+ body: { agent: maliciousAgent, args: {}, fuel: 50 },
1152
+ headers: { authorization: 'token_123' }
1153
+ })
1154
+
1155
+ console.log('Agent: while (true) { ... }')
1156
+ console.log('Error:', result2.error)
1157
+ console.log('Fuel used:', result2.fuelUsed, '(exhausted at limit)')
1158
+ console.log('Status: ✗ Safely terminated')
1159
+
1160
+
1161
+ // --- TEST 3: Complex computation (Metered) ---
1162
+ console.log('\\n═══════════════════════════════════════════')
1163
+ console.log('TEST 3: Complex Agent (Metered)')
1164
+ console.log('═══════════════════════════════════════════')
1165
+
1166
+ const complexAgent = \`
1167
+ function fibonacci({ n }) {
1168
+ if (n <= 1) return { result: n }
1169
+
1170
+ let a = 0
1171
+ let b = 1
1172
+ let i = 2
1173
+ while (i <= n) {
1174
+ let temp = a + b
1175
+ a = b
1176
+ b = temp
1177
+ i = i + 1
1178
+ }
1179
+ return { result: b, iterations: n }
1180
+ }
1181
+ \`
1182
+
1183
+ const result3 = await endpoint({
1184
+ body: { agent: complexAgent, args: { n: 20 }, fuel: 500 },
1185
+ headers: { authorization: 'token_123' }
1186
+ })
1187
+
1188
+ console.log('Agent: fibonacci({ n: 20 })')
1189
+ console.log('Result:', result3.result)
1190
+ console.log('Fuel used:', result3.fuelUsed)
1191
+ console.log('Status: ✓ Success (metered)')
1192
+
1193
+
1194
+ // --- THE PUNCHLINE ---
1195
+ console.log('\\n═══════════════════════════════════════════')
1196
+ console.log('THE PUNCHLINE')
1197
+ console.log('═══════════════════════════════════════════')
1198
+ console.log(\`
1199
+ Your current backend?
1200
+ - The infinite loop would have HUNG your server
1201
+ - Or cost you THOUSANDS on Lambda
1202
+ - Or crashed your Kubernetes pod
1203
+ - Or required a "senior engineer" to add timeout logic
1204
+
1205
+ Tosi?
1206
+ - Charged 50 fuel units
1207
+ - Returned an error
1208
+ - Kept running
1209
+ - Total code: 50 lines
1210
+
1211
+ This is the entire backend industry.
1212
+
1213
+ One endpoint.
1214
+ Any logic.
1215
+ Zero deployment.
1216
+ Everyone is full stack now.
1217
+ \`)`,
1218
+ },
1219
+ {
1220
+ name: 'Inline Tests: Test Private Functions',
1221
+ description: 'Test internals without exporting them - the killer feature',
1222
+ group: 'advanced',
1223
+ code: `/**
1224
+ * # Testing Private Functions
1225
+ *
1226
+ * This is the killer feature of inline tests:
1227
+ * You can test functions WITHOUT exporting them.
1228
+ *
1229
+ * Traditional testing requires you to either:
1230
+ * - Export internal helpers (pollutes your API)
1231
+ * - Test only through public interface (incomplete coverage)
1232
+ * - Use hacks like rewire/proxyquire (brittle)
1233
+ *
1234
+ * TJS inline tests have full access to the module scope.
1235
+ * Test everything. Export only what you need.
1236
+ */
1237
+
1238
+ // ============================================================
1239
+ // PRIVATE HELPERS (not exported, but fully testable!)
1240
+ // ============================================================
1241
+
1242
+ // Private: Email validation regex
1243
+ const EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/
1244
+
1245
+ // Private: Validate email format
1246
+ function isValidEmail(email: '') -> true {
1247
+ return EMAIL_REGEX.test(email)
1248
+ }
1249
+
1250
+ // Private: Sanitize user input
1251
+ function sanitize(input: '') -> '' {
1252
+ return input.trim().toLowerCase()
1253
+ }
1254
+
1255
+ // Private: Generate a unique ID
1256
+ function generateId(prefix: 'user') -> '' {
1257
+ return prefix + '_' + Math.random().toString(36).slice(2, 10)
1258
+ }
1259
+
1260
+ // Private: Hash password (simplified for demo)
1261
+ function hashPassword(password: '') -> '' {
1262
+ let hash = 0
1263
+ for (let i = 0; i < password.length; i++) {
1264
+ hash = ((hash << 5) - hash) + password.charCodeAt(i)
1265
+ hash = hash & hash
1266
+ }
1267
+ return 'hashed_' + Math.abs(hash).toString(16)
1268
+ }
1269
+
1270
+ // Private: Check password strength
1271
+ function isStrongPassword(password: '') -> { strong: true, issues: [''] } {
1272
+ const issues = []
1273
+ if (password.length < 8) issues.push('Must be at least 8 characters')
1274
+ if (!/[A-Z]/.test(password)) issues.push('Must contain uppercase letter')
1275
+ if (!/[a-z]/.test(password)) issues.push('Must contain lowercase letter')
1276
+ if (!/[0-9]/.test(password)) issues.push('Must contain a number')
1277
+ return { strong: issues.length === 0, issues }
1278
+ }
1279
+
1280
+ // ============================================================
1281
+ // PUBLIC API (this is all that gets exported)
1282
+ // ============================================================
1283
+
1284
+ export function createUser(input: { email: '', password: '' })
1285
+ -> { id: '', email: '', passwordHash: '' } | { error: '', code: 0 } {
1286
+
1287
+ // Validate email (using private helper)
1288
+ const cleanEmail = sanitize(input.email)
1289
+ if (!isValidEmail(cleanEmail)) {
1290
+ return { error: 'Invalid email format', code: 400 }
1291
+ }
1292
+
1293
+ // Validate password (using private helper)
1294
+ const strength = isStrongPassword(input.password)
1295
+ if (!strength.strong) {
1296
+ return { error: strength.issues.join(', '), code: 400 }
1297
+ }
1298
+
1299
+ // Create user (using private helpers)
1300
+ return {
1301
+ id: generateId('user'),
1302
+ email: cleanEmail,
1303
+ passwordHash: hashPassword(input.password)
1304
+ }
1305
+ }
1306
+
1307
+ // ============================================================
1308
+ // TESTS - Full access to private functions!
1309
+ // ============================================================
1310
+
1311
+ // --- Test private email validation ---
1312
+ test('isValidEmail accepts valid emails') {
1313
+ expect(isValidEmail('test@example.com')).toBe(true)
1314
+ expect(isValidEmail('user.name+tag@domain.co.uk')).toBe(true)
1315
+ }
1316
+
1317
+ test('isValidEmail rejects invalid emails') {
1318
+ expect(isValidEmail('not-an-email')).toBe(false)
1319
+ expect(isValidEmail('@nodomain.com')).toBe(false)
1320
+ expect(isValidEmail('spaces in@email.com')).toBe(false)
1321
+ }
1322
+
1323
+ // --- Test private sanitization ---
1324
+ test('sanitize trims and lowercases') {
1325
+ expect(sanitize(' HELLO ')).toBe('hello')
1326
+ expect(sanitize(' Test@Email.COM ')).toBe('test@email.com')
1327
+ }
1328
+
1329
+ // --- Test private ID generation ---
1330
+ test('generateId creates prefixed unique IDs') {
1331
+ const id1 = generateId('user')
1332
+ const id2 = generateId('user')
1333
+ expect(id1.startsWith('user_')).toBe(true)
1334
+ expect(id1).not.toBe(id2) // unique each time
1335
+ }
1336
+
1337
+ test('generateId respects prefix') {
1338
+ expect(generateId('post').startsWith('post_')).toBe(true)
1339
+ expect(generateId('comment').startsWith('comment_')).toBe(true)
1340
+ }
1341
+
1342
+ // --- Test private password hashing ---
1343
+ test('hashPassword is deterministic') {
1344
+ const hash1 = hashPassword('secret123')
1345
+ const hash2 = hashPassword('secret123')
1346
+ expect(hash1).toBe(hash2)
1347
+ }
1348
+
1349
+ test('hashPassword produces different hashes for different inputs') {
1350
+ const hash1 = hashPassword('password1')
1351
+ const hash2 = hashPassword('password2')
1352
+ expect(hash1).not.toBe(hash2)
1353
+ }
1354
+
1355
+ // --- Test private password strength checker ---
1356
+ test('isStrongPassword rejects weak passwords') {
1357
+ const result = isStrongPassword('weak')
1358
+ expect(result.strong).toBe(false)
1359
+ expect(result.issues.length).toBeGreaterThan(0)
1360
+ }
1361
+
1362
+ test('isStrongPassword accepts strong passwords') {
1363
+ const result = isStrongPassword('MyStr0ngP@ss!')
1364
+ expect(result.strong).toBe(true)
1365
+ expect(result.issues.length).toBe(0)
1366
+ }
1367
+
1368
+ test('isStrongPassword lists specific issues') {
1369
+ const noUpper = isStrongPassword('lowercase123')
1370
+ expect(noUpper.issues).toContain('Must contain uppercase letter')
1371
+
1372
+ const noLower = isStrongPassword('UPPERCASE123')
1373
+ expect(noLower.issues).toContain('Must contain lowercase letter')
1374
+
1375
+ const noNumber = isStrongPassword('NoNumbers!')
1376
+ expect(noNumber.issues).toContain('Must contain a number')
1377
+
1378
+ const tooShort = isStrongPassword('Ab1!')
1379
+ expect(tooShort.issues).toContain('Must be at least 8 characters')
1380
+ }
1381
+
1382
+ // --- Test the public API (integration) ---
1383
+ test('createUser validates email') {
1384
+ const result = createUser({ email: 'invalid', password: 'StrongPass1!' })
1385
+ expect(result.error).toBe('Invalid email format')
1386
+ }
1387
+
1388
+ test('createUser validates password strength') {
1389
+ const result = createUser({ email: 'test@test.com', password: 'weak' })
1390
+ expect(result.error).toBeTruthy()
1391
+ }
1392
+
1393
+ test('createUser succeeds with valid input') {
1394
+ const result = createUser({
1395
+ email: ' Test@Example.COM ',
1396
+ password: 'MyStr0ngPass!'
1397
+ })
1398
+ expect(result.id).toBeTruthy()
1399
+ expect(result.email).toBe('test@example.com') // sanitized
1400
+ expect(result.passwordHash.startsWith('hashed_')).toBe(true)
1401
+ }
1402
+
1403
+ // ============================================================
1404
+ // DEMO OUTPUT
1405
+ // ============================================================
1406
+
1407
+ console.log('=== Testing Private Functions Demo ===\\n')
1408
+ console.log('The functions isValidEmail, sanitize, generateId,')
1409
+ console.log('hashPassword, and isStrongPassword are all PRIVATE.')
1410
+ console.log('They are NOT exported. But we tested them all!\\n')
1411
+
1412
+ console.log('Try this in Jest/Vitest without exporting them. You can\\'t.')
1413
+ console.log('You\\'d have to either pollute your API or leave them untested.\\n')
1414
+
1415
+ console.log('TJS inline tests: Full coverage. Clean exports.\\n')
1416
+
1417
+ // Show the public API working
1418
+ const user = createUser({ email: 'demo@example.com', password: 'SecurePass123!' })
1419
+ console.log('Created user:', user)`,
1420
+ },
1421
+ ]
1422
+
1423
+ // Types for docs
1424
+ interface DocItem {
1425
+ title: string
1426
+ filename: string
1427
+ text: string
1428
+ category?: 'ajs' | 'tjs' | 'general'
1429
+ hidden?: boolean
1430
+ }
1431
+
1432
+ interface DemoNavEvents {
1433
+ 'select-ajs-example': { example: (typeof ajsExamples)[0] }
1434
+ 'select-tjs-example': { example: (typeof tjsExamples)[0] }
1435
+ 'select-ts-example': { example: TSExample }
1436
+ 'select-doc': { doc: DocItem }
1437
+ }
1438
+
1439
+ export class DemoNav extends Component {
1440
+ private _docs: DocItem[] = []
1441
+ private openSection: string | null = null
1442
+ private floatViewer: XinFloat | null = null
1443
+ private mdViewer: MarkdownViewer | null = null
1444
+
1445
+ // Track current selection for highlighting
1446
+ private _currentView: 'home' | 'ajs' | 'tjs' = 'home'
1447
+ private _currentExample: string | null = null
1448
+
1449
+ constructor() {
1450
+ super()
1451
+ // Initialize from URL hash
1452
+ this.loadStateFromURL()
1453
+ // Listen for hash changes
1454
+ window.addEventListener('hashchange', () => this.loadStateFromURL())
1455
+ }
1456
+
1457
+ get currentView() {
1458
+ return this._currentView
1459
+ }
1460
+
1461
+ set currentView(value: 'home' | 'ajs' | 'tjs') {
1462
+ this._currentView = value
1463
+ // Auto-open the appropriate section
1464
+ if (value === 'ajs') {
1465
+ this.openSection = 'ajs-demos'
1466
+ } else if (value === 'tjs') {
1467
+ this.openSection = 'tjs-demos'
1468
+ }
1469
+ this.rebuildNav()
1470
+ // Update indicator after rebuild (DOM now exists)
1471
+ this.updateCurrentIndicator()
1472
+ }
1473
+
1474
+ get currentExample() {
1475
+ return this._currentExample
1476
+ }
1477
+
1478
+ set currentExample(value: string | null) {
1479
+ this._currentExample = value
1480
+ this.updateCurrentIndicator()
1481
+ }
1482
+
1483
+ private updateCurrentIndicator() {
1484
+ // Update .current class on nav items
1485
+ const items = this.querySelectorAll('.nav-item')
1486
+ items.forEach((item) => {
1487
+ const itemName = item.textContent?.trim()
1488
+ const isCurrent = itemName === this._currentExample
1489
+ item.classList.toggle('current', isCurrent)
1490
+ })
1491
+ // Update home link
1492
+ const homeLink = this.querySelector('.home-link')
1493
+ homeLink?.classList.toggle('current', this._currentView === 'home')
1494
+ }
1495
+
1496
+ private loadStateFromURL() {
1497
+ const hash = window.location.hash.slice(1) // Remove '#'
1498
+ if (!hash) return
1499
+
1500
+ const params = new URLSearchParams(hash)
1501
+ const section = params.get('section')
1502
+ const view = params.get('view')
1503
+ const example = params.get('example')
1504
+
1505
+ // Set view and open appropriate section
1506
+ if (view === 'ajs') {
1507
+ this._currentView = 'ajs'
1508
+ this.openSection = 'ajs-demos'
1509
+ } else if (view === 'tjs') {
1510
+ this._currentView = 'tjs'
1511
+ this.openSection = 'tjs-demos'
1512
+ } else if (view === 'home') {
1513
+ this._currentView = 'home'
1514
+ } else if (
1515
+ section &&
1516
+ ['ajs-demos', 'tjs-demos', 'ajs-docs', 'tjs-docs'].includes(section)
1517
+ ) {
1518
+ this.openSection = section
1519
+ }
1520
+
1521
+ // Set current example for highlighting
1522
+ if (example) {
1523
+ this._currentExample = example
1524
+ }
1525
+
1526
+ this.rebuildNav()
1527
+ this.updateCurrentIndicator()
1528
+ }
1529
+
1530
+ private saveStateToURL() {
1531
+ const params = new URLSearchParams(window.location.hash.slice(1))
1532
+ if (this.openSection) {
1533
+ params.set('section', this.openSection)
1534
+ }
1535
+ const newHash = params.toString()
1536
+ if (newHash !== window.location.hash.slice(1)) {
1537
+ window.history.replaceState(null, '', `#${newHash}`)
1538
+ }
1539
+ }
1540
+
1541
+ get docs(): DocItem[] {
1542
+ return this._docs
1543
+ }
1544
+
1545
+ set docs(value: DocItem[]) {
1546
+ this._docs = value
1547
+ // Re-render when docs are set
1548
+ this.rebuildNav()
1549
+ }
1550
+
1551
+ // Light DOM styles (no static styleSpec)
1552
+ static lightDOMStyles = {
1553
+ ':host': {
1554
+ display: 'flex',
1555
+ flexDirection: 'column',
1556
+ height: '100%',
1557
+ overflow: 'hidden',
1558
+ },
1559
+
1560
+ '.nav-sections': {
1561
+ flex: '1 1 auto',
1562
+ overflowY: 'auto',
1563
+ padding: '8px',
1564
+ },
1565
+
1566
+ details: {
1567
+ marginBottom: '4px',
1568
+ borderRadius: '6px',
1569
+ overflow: 'hidden',
1570
+ },
1571
+
1572
+ summary: {
1573
+ padding: '8px 12px',
1574
+ background: vars.codeBackground,
1575
+ color: vars.textColor,
1576
+ cursor: 'pointer',
1577
+ fontWeight: '500',
1578
+ fontSize: '14px',
1579
+ display: 'flex',
1580
+ alignItems: 'center',
1581
+ gap: '8px',
1582
+ userSelect: 'none',
1583
+ listStyle: 'none',
1584
+ },
1585
+
1586
+ 'summary::-webkit-details-marker': {
1587
+ display: 'none',
1588
+ },
1589
+
1590
+ 'summary::before': {
1591
+ content: '"▶"',
1592
+ fontSize: '10px',
1593
+ transition: 'transform 0.2s',
1594
+ },
1595
+
1596
+ 'details[open] summary::before': {
1597
+ transform: 'rotate(90deg)',
1598
+ },
1599
+
1600
+ 'summary:hover': {
1601
+ background: vars.codeBorder,
1602
+ },
1603
+
1604
+ '.section-content': {
1605
+ padding: '4px 0',
1606
+ },
1607
+
1608
+ '.nav-item': {
1609
+ display: 'block',
1610
+ padding: '6px 12px 6px 24px',
1611
+ cursor: 'pointer',
1612
+ fontSize: '13px',
1613
+ color: vars.textColor,
1614
+ textDecoration: 'none',
1615
+ borderRadius: '4px',
1616
+ transition: 'background 0.15s',
1617
+ },
1618
+
1619
+ '.nav-item:hover': {
1620
+ background: vars.codeBackground,
1621
+ },
1622
+
1623
+ '.nav-item.requires-api::after': {
1624
+ content: '"🔑"',
1625
+ marginLeft: '4px',
1626
+ fontSize: '11px',
1627
+ },
1628
+
1629
+ '.nav-item.current': {
1630
+ background: vars.brandColor,
1631
+ fontWeight: '500',
1632
+ color: '#fff',
1633
+ },
1634
+
1635
+ '.group-header': {
1636
+ padding: '8px 12px 4px 16px',
1637
+ fontSize: '11px',
1638
+ fontWeight: '600',
1639
+ color: vars.textColorLight,
1640
+ textTransform: 'uppercase',
1641
+ letterSpacing: '0.5px',
1642
+ },
1643
+
1644
+ '.group-header:not(:first-child)': {
1645
+ marginTop: '8px',
1646
+ borderTop: `1px solid ${vars.codeBorder}`,
1647
+ paddingTop: '12px',
1648
+ },
1649
+
1650
+ '.section-icon': {
1651
+ width: '16px',
1652
+ height: '16px',
1653
+ },
1654
+
1655
+ '.home-link': {
1656
+ display: 'flex',
1657
+ alignItems: 'center',
1658
+ gap: '8px',
1659
+ padding: '10px 12px',
1660
+ marginBottom: '8px',
1661
+ cursor: 'pointer',
1662
+ fontSize: '14px',
1663
+ fontWeight: '500',
1664
+ color: '#374151',
1665
+ borderRadius: '6px',
1666
+ transition: 'background 0.15s',
1667
+ },
1668
+
1669
+ '.home-link:hover': {
1670
+ background: '#f3f4f6',
1671
+ },
1672
+
1673
+ '.home-link.current': {
1674
+ background: '#e0e7ff',
1675
+ color: '#3730a3',
1676
+ },
1677
+ }
1678
+
1679
+ content = () => [div({ class: 'nav-sections', part: 'sections' })]
1680
+
1681
+ connectedCallback() {
1682
+ super.connectedCallback()
1683
+ this.rebuildNav()
1684
+ // Update indicator after DOM is ready
1685
+ this.updateCurrentIndicator()
1686
+ }
1687
+
1688
+ // Group labels for display
1689
+ private static readonly GROUP_LABELS: Record<string, string> = {
1690
+ // TypeScript example groups
1691
+ intro: 'Introduction',
1692
+ validation: 'Runtime Validation',
1693
+ // TJS example groups
1694
+
1695
+ featured: 'Featured',
1696
+ basics: 'Basics',
1697
+ patterns: 'Patterns',
1698
+ api: 'API',
1699
+ llm: 'LLM',
1700
+ fullstack: 'Full Stack',
1701
+ advanced: 'Advanced',
1702
+ }
1703
+
1704
+ // Group ordering (featured first, then alphabetical-ish)
1705
+ private static readonly GROUP_ORDER = [
1706
+ // TypeScript groups
1707
+ 'intro',
1708
+ 'validation',
1709
+ // TJS groups
1710
+ 'featured',
1711
+ 'basics',
1712
+ 'patterns',
1713
+ 'api',
1714
+ 'llm',
1715
+ 'fullstack',
1716
+ 'advanced',
1717
+ ]
1718
+
1719
+ // Helper to render examples grouped by their group field
1720
+ private renderGroupedExamples<T extends { name: string; group?: string }>(
1721
+ examples: T[],
1722
+ renderItem: (ex: T) => HTMLElement
1723
+ ): HTMLElement[] {
1724
+ const grouped = new Map<string, T[]>()
1725
+
1726
+ // Group examples
1727
+ for (const ex of examples) {
1728
+ const group = ex.group || 'other'
1729
+ if (!grouped.has(group)) {
1730
+ grouped.set(group, [])
1731
+ }
1732
+ grouped.get(group)!.push(ex)
1733
+ }
1734
+
1735
+ // Sort groups by GROUP_ORDER
1736
+ const sortedGroups = Array.from(grouped.keys()).sort((a, b) => {
1737
+ const orderA = DemoNav.GROUP_ORDER.indexOf(a)
1738
+ const orderB = DemoNav.GROUP_ORDER.indexOf(b)
1739
+ return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB)
1740
+ })
1741
+
1742
+ // Render groups with headers
1743
+ const elements: HTMLElement[] = []
1744
+ for (const group of sortedGroups) {
1745
+ const items = grouped.get(group)!
1746
+ const label = DemoNav.GROUP_LABELS[group] || group
1747
+
1748
+ // Add group header
1749
+ elements.push(div({ class: 'group-header' }, label))
1750
+
1751
+ // Add items in this group
1752
+ for (const ex of items) {
1753
+ elements.push(renderItem(ex))
1754
+ }
1755
+ }
1756
+
1757
+ return elements
1758
+ }
1759
+
1760
+ rebuildNav() {
1761
+ const container = this.querySelector('.nav-sections')
1762
+ if (!container) return
1763
+
1764
+ container.innerHTML = ''
1765
+ container.append(
1766
+ // Home link
1767
+ div(
1768
+ {
1769
+ class:
1770
+ this._currentView === 'home' ? 'home-link current' : 'home-link',
1771
+ onClick: () => this.selectHome(),
1772
+ },
1773
+ span({ class: 'section-icon' }, icons.home({ size: 16 })),
1774
+ 'Home'
1775
+ ),
1776
+
1777
+ // TypeScript Examples (TS -> TJS -> JS pipeline)
1778
+ details(
1779
+ {
1780
+ open: this.openSection === 'ts-demos',
1781
+ 'data-section': 'ts-demos',
1782
+ onToggle: this.handleToggle,
1783
+ },
1784
+ summary(
1785
+ span({ class: 'section-icon' }, icons.code({ size: 16 })),
1786
+ 'TypeScript Examples'
1787
+ ),
1788
+ div(
1789
+ { class: 'section-content' },
1790
+ ...this.renderGroupedExamples(tsExamples, (ex) =>
1791
+ div(
1792
+ {
1793
+ class: 'nav-item',
1794
+ title: ex.description,
1795
+ onClick: () => this.selectTsExample(ex),
1796
+ },
1797
+ ex.name
1798
+ )
1799
+ )
1800
+ )
1801
+ ),
1802
+
1803
+ // TJS Examples
1804
+ details(
1805
+ {
1806
+ open: this.openSection === 'tjs-demos',
1807
+ 'data-section': 'tjs-demos',
1808
+ onToggle: this.handleToggle,
1809
+ },
1810
+ summary(
1811
+ span({ class: 'section-icon' }, icons.code({ size: 16 })),
1812
+ 'TJS Examples'
1813
+ ),
1814
+ div(
1815
+ { class: 'section-content' },
1816
+ ...this.renderGroupedExamples(tjsExamples, (ex) =>
1817
+ div(
1818
+ {
1819
+ class: 'nav-item',
1820
+ title: ex.description,
1821
+ onClick: () => this.selectTjsExample(ex),
1822
+ },
1823
+ ex.name
1824
+ )
1825
+ )
1826
+ )
1827
+ ),
1828
+
1829
+ // AJS Examples
1830
+ details(
1831
+ {
1832
+ open: this.openSection === 'ajs-demos',
1833
+ 'data-section': 'ajs-demos',
1834
+ onToggle: this.handleToggle,
1835
+ },
1836
+ summary(
1837
+ span({ class: 'section-icon' }, icons.code({ size: 16 })),
1838
+ 'AJS Examples'
1839
+ ),
1840
+ div(
1841
+ { class: 'section-content' },
1842
+ ...this.renderGroupedExamples(ajsExamples, (ex) =>
1843
+ div(
1844
+ {
1845
+ class: ex.requiresApi ? 'nav-item requires-api' : 'nav-item',
1846
+ title: ex.description,
1847
+ onClick: () => this.selectAjsExample(ex),
1848
+ },
1849
+ ex.name
1850
+ )
1851
+ )
1852
+ )
1853
+ ),
1854
+
1855
+ // TJS Docs
1856
+ details(
1857
+ {
1858
+ open: this.openSection === 'tjs-docs',
1859
+ 'data-section': 'tjs-docs',
1860
+ onToggle: this.handleToggle,
1861
+ },
1862
+ summary(
1863
+ span({ class: 'section-icon' }, icons.book({ size: 16 })),
1864
+ 'TJS Docs'
1865
+ ),
1866
+ div(
1867
+ { class: 'section-content' },
1868
+ ...this.getTjsDocs().map((doc) =>
1869
+ div(
1870
+ {
1871
+ class: 'nav-item',
1872
+ onClick: () => this.selectDoc(doc),
1873
+ },
1874
+ doc.title
1875
+ )
1876
+ )
1877
+ )
1878
+ ),
1879
+
1880
+ // AJS Docs
1881
+ details(
1882
+ {
1883
+ open: this.openSection === 'ajs-docs',
1884
+ 'data-section': 'ajs-docs',
1885
+ onToggle: this.handleToggle,
1886
+ },
1887
+ summary(
1888
+ span({ class: 'section-icon' }, icons.book({ size: 16 })),
1889
+ 'AJS Docs'
1890
+ ),
1891
+ div(
1892
+ { class: 'section-content' },
1893
+ ...this.getAjsDocs().map((doc) =>
1894
+ div(
1895
+ {
1896
+ class: 'nav-item',
1897
+ onClick: () => this.selectDoc(doc),
1898
+ },
1899
+ doc.title
1900
+ )
1901
+ )
1902
+ )
1903
+ )
1904
+ )
1905
+ }
1906
+
1907
+ handleToggle = (event: Event) => {
1908
+ const details = event.target as HTMLDetailsElement
1909
+ const section = details.getAttribute('data-section')
1910
+
1911
+ if (details.open) {
1912
+ // Close other sections (accordion behavior)
1913
+ this.openSection = section
1914
+ const allDetails = this.querySelectorAll('details')
1915
+ allDetails.forEach((d) => {
1916
+ if (d !== details && d.open) {
1917
+ d.open = false
1918
+ }
1919
+ })
1920
+ // Save to URL
1921
+ this.saveStateToURL()
1922
+ }
1923
+ }
1924
+
1925
+ getAjsDocs(): DocItem[] {
1926
+ return this.docs.filter(
1927
+ (d) =>
1928
+ !d.hidden &&
1929
+ (d.filename.includes('ASYNCJS') ||
1930
+ d.filename.includes('PATTERNS') ||
1931
+ d.filename === 'runtime.ts')
1932
+ )
1933
+ }
1934
+
1935
+ getTjsDocs(): DocItem[] {
1936
+ return this.docs.filter(
1937
+ (d) =>
1938
+ !d.hidden &&
1939
+ (d.filename.includes('TJS') ||
1940
+ d.filename === 'CONTEXT.md' ||
1941
+ d.filename === 'PLAN.md')
1942
+ )
1943
+ }
1944
+
1945
+ selectHome() {
1946
+ this._currentView = 'home'
1947
+ this._currentExample = null
1948
+ this.updateCurrentIndicator()
1949
+ this.dispatchEvent(
1950
+ new CustomEvent('select-home', {
1951
+ bubbles: true,
1952
+ })
1953
+ )
1954
+ }
1955
+
1956
+ selectAjsExample(example: (typeof ajsExamples)[0]) {
1957
+ this._currentView = 'ajs'
1958
+ this._currentExample = example.name
1959
+ this.updateCurrentIndicator()
1960
+ this.dispatchEvent(
1961
+ new CustomEvent('select-ajs-example', {
1962
+ detail: { example },
1963
+ bubbles: true,
1964
+ })
1965
+ )
1966
+ }
1967
+
1968
+ selectTjsExample(example: (typeof tjsExamples)[0]) {
1969
+ this._currentView = 'tjs'
1970
+ this._currentExample = example.name
1971
+ this.updateCurrentIndicator()
1972
+ this.dispatchEvent(
1973
+ new CustomEvent('select-tjs-example', {
1974
+ detail: { example },
1975
+ bubbles: true,
1976
+ })
1977
+ )
1978
+ }
1979
+
1980
+ selectTsExample(example: TSExample) {
1981
+ this._currentView = 'tjs' // Will switch to 'ts' when TS playground is wired up
1982
+ this._currentExample = example.name
1983
+ this.updateCurrentIndicator()
1984
+ this.dispatchEvent(
1985
+ new CustomEvent('select-ts-example', {
1986
+ detail: { example },
1987
+ bubbles: true,
1988
+ })
1989
+ )
1990
+ }
1991
+
1992
+ selectDoc(doc: DocItem) {
1993
+ // Open or update floating doc viewer
1994
+ if (!this.floatViewer || !document.body.contains(this.floatViewer)) {
1995
+ this.createFloatViewer(doc)
1996
+ } else {
1997
+ // Update existing viewer
1998
+ if (this.mdViewer) {
1999
+ this.mdViewer.value = doc.text
2000
+ }
2001
+ // Update title
2002
+ const title = this.floatViewer.querySelector('.float-title')
2003
+ if (title) {
2004
+ title.textContent = doc.title
2005
+ }
2006
+ }
2007
+
2008
+ this.dispatchEvent(
2009
+ new CustomEvent('select-doc', {
2010
+ detail: { doc },
2011
+ bubbles: true,
2012
+ })
2013
+ )
2014
+ }
2015
+
2016
+ createFloatViewer(doc: DocItem) {
2017
+ this.mdViewer = markdownViewer({
2018
+ class: 'no-drag markdown-content',
2019
+ value: doc.text,
2020
+ style: {
2021
+ display: 'block',
2022
+ padding: '4px 20px 12px',
2023
+ overflow: 'auto',
2024
+ maxHeight: 'calc(80vh - 40px)',
2025
+ },
2026
+ })
2027
+
2028
+ const closeBtn = button(
2029
+ {
2030
+ class: 'iconic no-drag',
2031
+ style: {
2032
+ padding: '4px',
2033
+ border: 'none',
2034
+ background: 'transparent',
2035
+ cursor: 'pointer',
2036
+ },
2037
+ },
2038
+ icons.x({ size: 16 })
2039
+ )
2040
+
2041
+ this.floatViewer = xinFloat(
2042
+ {
2043
+ drag: true,
2044
+ remainOnResize: 'remain',
2045
+ remainOnScroll: 'remain',
2046
+ style: {
2047
+ position: 'fixed',
2048
+ top: '60px',
2049
+ right: '20px',
2050
+ width: '500px',
2051
+ maxWidth: 'calc(100vw - 40px)',
2052
+ maxHeight: '80vh',
2053
+ background: 'white',
2054
+ borderRadius: '8px',
2055
+ boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
2056
+ overflow: 'hidden',
2057
+ zIndex: '1000',
2058
+ },
2059
+ },
2060
+ // Header
2061
+ div(
2062
+ {
2063
+ style: {
2064
+ display: 'flex',
2065
+ alignItems: 'center',
2066
+ padding: '6px 12px',
2067
+ background: '#f3f4f6',
2068
+ borderBottom: '1px solid #e5e7eb',
2069
+ cursor: 'move',
2070
+ },
2071
+ },
2072
+ span(
2073
+ { class: 'float-title', style: { flex: '1', fontWeight: '500' } },
2074
+ doc.title
2075
+ ),
2076
+ closeBtn
2077
+ ),
2078
+ // Content
2079
+ this.mdViewer
2080
+ )
2081
+
2082
+ // Add click handler after element is created
2083
+ closeBtn.addEventListener('click', (e) => {
2084
+ e.stopPropagation()
2085
+ this.floatViewer?.remove()
2086
+ this.floatViewer = null
2087
+ this.mdViewer = null
2088
+ })
2089
+
2090
+ document.body.appendChild(this.floatViewer)
2091
+ }
2092
+ }
2093
+
2094
+ export const demoNav: ElementCreator<DemoNav> = DemoNav.elementCreator({
2095
+ tag: 'demo-nav',
2096
+ styleSpec: DemoNav.lightDOMStyles,
2097
+ })