tjs-lang 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/demo/docs.json CHANGED
@@ -681,6 +681,12 @@
681
681
  "filename": "from-ts.ts",
682
682
  "path": "src/lang/emitters/from-ts.ts"
683
683
  },
684
+ {
685
+ "title": "GEMINI.md - Project Context & Instructions",
686
+ "filename": "GEMINI.md",
687
+ "path": "GEMINI.md",
688
+ "text": "# GEMINI.md - Project Context & Instructions\n\nThis file provides foundational mandates and contextual information for Gemini CLI when working in the `tjs-lang` repository.\n\n## Project Overview\n\n**tjs-lang** is a platform for type-safe JavaScript execution and AI agent orchestration. It bridges the gap between static typing and runtime safety by preserving type information and providing a secure execution environment.\n\n### Core Languages\n\n1. **TJS (Typed JavaScript):** A TypeScript-like dialect that transpiles to JavaScript with embedded `__tjs` metadata. This metadata enables runtime type validation, auto-documentation, and introspection.\n2. **AJS (Agent JavaScript):** A safe, sandboxed language for AI agents. It compiles to a JSON-serializable AST and executes in a gas-limited VM with capability-based security.\n\n### Architecture\n\n- **Builder Layer** (`src/builder.ts`): Fluent API for constructing AST nodes.\n- **Runtime Layer** (`src/vm/runtime.ts`): Security-critical core that executes AST nodes and manages fuel/capabilities.\n- **Lang Layer** (`src/lang/`): Contains the parser (based on Acorn), linter, and emitters for TJS and AJS.\n- **Batteries** (`src/batteries/`): Built-in capabilities like LLM integration (LM Studio), vector search, and persistence.\n\n## Development Lifecycle\n\n### Common Commands\n\n- **Install:** `bun install`\n- **Build:** `npm run make` (Format, build grammars, compile TS, and bundle)\n- **Fast Test:** `npm run test:fast` (Runs core tests, skips LLM and benchmarks)\n- **Full Test:** `bun test`\n- **Type Check:** `npm run typecheck`\n- **Lint:** `npm run lint`\n- **Format:** `npm run format` (ESLint fix + Prettier)\n- **Dev Server:** `npm run dev`\n- **CLI Tool:** `bun src/cli/tjs.ts <command>` (Commands: `check`, `run`, `types`, `emit`, `convert`, `test`)\n\n### Testing Strategy\n\n- **Unit Tests:** Located alongside source files (e.g., `src/lang/parser.test.ts`).\n- **Integration Tests:** Located in `src/use-cases/`.\n- **Security Tests:** Critical for the VM, found in `src/use-cases/malicious-actor.test.ts`.\n- **Reproduction:** Always create a reproduction test case before fixing a bug.\n\n## Engineering Standards & Conventions\n\n### Coding Style\n\n- **Formatting:** Prettier (Single quotes, no semicolons, 2-space indentation, 80 char width).\n- **Naming:** Follow existing conventions; prefix unused variables with `_`.\n- **Type Safety:** Prioritize TypeScript for all core logic. `any` is allowed but should be used judiciously.\n- **Security:** Rigorously protect the `src/vm/runtime.ts` file as it is security-critical. Avoid prototype access (`__proto__`, `constructor`, `prototype`) in expression evaluation.\n\n### Language Specifics\n\n- **TJS Types:** In TJS, colons indicate _examples_, not just type annotations (e.g., `name: 'Alice'` means a required string).\n- **AJS Expressions:** Be aware that AJS expressions have differences from JS (e.g., safe null member access, limited computed member access).\n- **Monadic Errors:** Prefer returning error values (Monadic errors) over throwing exceptions to ensure execution stability and security.\n\n### Documentation\n\n- Maintain `DOCS-TJS.md` and `DOCS-AJS.md` for language-specific documentation.\n- Update `CLAUDE.md` for tool-specific guidance if architectural patterns change.\n\n## Contextual Precedence\n\n- **Directives:** Explicit requests for implementation take priority.\n- **Inquiries:** Requests for analysis or advice should not result in code changes without a subsequent Directive.\n- **Security First:** Never compromise the sandboxing or fuel-metering integrity of the VM.\n"
689
+ },
684
690
  {
685
691
  "title": "icebox",
686
692
  "filename": "tasks-f6d70f.md",
@@ -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[] = []
@@ -79,6 +79,8 @@ export interface IframeDocOptions {
79
79
  parentBindings?: boolean
80
80
  /** Auto-find and call TJS-annotated functions, append DOM results */
81
81
  autoCallTjsFunction?: boolean
82
+ /** Whether parent is in dark mode — sets color-scheme on iframe */
83
+ darkMode?: boolean
82
84
  }
83
85
 
84
86
  /**
@@ -95,8 +97,11 @@ export function buildIframeDoc(options: IframeDocOptions): string {
95
97
  importStatements = [],
96
98
  parentBindings = false,
97
99
  autoCallTjsFunction = false,
100
+ darkMode = false,
98
101
  } = options
99
102
 
103
+ const colorScheme = darkMode ? 'dark' : 'light dark'
104
+
100
105
  const parentBindingsScript = parentBindings
101
106
  ? `
102
107
  if (parent.run) window.run = parent.run.bind(parent);
@@ -146,6 +151,7 @@ export function buildIframeDoc(options: IframeDocOptions): string {
146
151
  return `<!DOCTYPE html>
147
152
  <html>
148
153
  <head>
154
+ <style>:root { color-scheme: ${colorScheme} }</style>
149
155
  <style>${cssContent}</style>
150
156
  ${importMapScript}
151
157
  </head>
@@ -174,6 +180,7 @@ ${CONSOLE_CAPTURE_SCRIPT}
174
180
  return `<!DOCTYPE html>
175
181
  <html>
176
182
  <head>
183
+ <style>:root { color-scheme: ${colorScheme} }</style>
177
184
  <style>${cssContent}</style>
178
185
  ${importMapScript}
179
186
  </head>
@@ -1034,6 +1034,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
1034
1034
  importStatements,
1035
1035
  parentBindings: true,
1036
1036
  autoCallTjsFunction: true,
1037
+ darkMode: document.body.classList.contains('darkmode'),
1037
1038
  })
1038
1039
 
1039
1040
  // Listen for messages from iframe
@@ -513,6 +513,7 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
513
513
  htmlContent,
514
514
  importMapScript,
515
515
  jsCode,
516
+ darkMode: document.body.classList.contains('darkmode'),
516
517
  })
517
518
 
518
519
  // Clean up any previous message handler