tosijs-ui 1.6.21 → 1.6.23

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.
@@ -237,7 +237,6 @@ export declare class LiveExample extends Component<ExampleParts> {
237
237
  private beforeUnloadHandler?;
238
238
  static insertExamples(element: HTMLElement, context?: ExampleContext, sourceFile?: string): void;
239
239
  get activeTab(): Element | undefined;
240
- private get hydrated();
241
240
  private getEditorValue;
242
241
  private setEditorValue;
243
242
  private flushPendingValues;
@@ -33,38 +33,6 @@ preview.append('Try editing some code and hitting refresh…')
33
33
  }
34
34
  ```
35
35
 
36
- ## How examples run: one shared page (read this)
37
-
38
- By default every example on a page runs **inline — in the page's own document and
39
- JavaScript realm**, not in a sandbox. Each example gets its own `preview` element
40
- (and its `css` is scoped to that preview), but they all share the one `tosijs`
41
- module, so **`tosi()` state singletons are shared across every example on the page**.
42
-
43
- This is a deliberate choice, and it buys two things:
44
-
45
- 1. It's a live demonstration of how clean tosi's isolation is — many independent
46
- components mount into one page and coexist without stepping on each other's DOM.
47
- 2. You can drive any demo from the console (or from another example) through the same
48
- global singleton — the state is right there, not walled off in a frame.
49
-
50
- The trade-off is the gotcha to know about: **examples can see and clobber each
51
- other's state.** Two examples that both do `tosi({ app: … })` bind to the *same*
52
- `app` singleton (the tosi registry is keyed by the top-level name), so one overwrites
53
- the other — and you get "impossible" bugs where an example misbehaves only when some
54
- *other* example happens to be on the same page. Worth it, but plan for it:
55
-
56
- - **Namespace your state.** Give each example a unique top-level key
57
- (`tosi({ ratingDemo: … })`, not a generic `tosi({ app: … })`).
58
- - Prefer local variables and `preview`-scoped DOM over shared singletons when a demo
59
- doesn't need to be globally reachable.
60
- - In `test` blocks, assert with **counts / deltas**, not presence/absence — other
61
- examples may have left their elements in the DOM.
62
-
63
- For real isolation of the **DOM and CSS**, add the `iframe` attribute (below). Note
64
- it does **not** isolate tosijs *state*: the `tosijs`/`tosijs-ui` handed to an iframe
65
- example are still the host page's module instances, so `tosi()` singletons stay
66
- shared. Namespacing is the fix for state; `iframe` is the fix for DOM/CSS bleed.
67
-
68
36
  ## Source dialects: `js`, `tjs`, `ts`
69
37
 
70
38
  The executable block's fence language picks how the source is compiled before it
@@ -108,157 +76,9 @@ example the equivalent is written inside a comment (so it survives `tsc`) — bu
108
76
  that comment form can't appear inside a doc comment like this one, since the
109
77
  test's own closing delimiter would end the doc comment.
110
78
 
111
- ## Inline WebAssembly (SIMD)
112
-
113
- A `tjs` example can drop a hot loop into **WebAssembly** with a `wasm { … } fallback
114
- { … }` block — compiled to bytecode *at transpile time*, embedded as base64, and run
115
- in your browser. No Emscripten, no `.wasm` file, no build step, no server.
116
-
117
- WASM earns its keep with **SIMD**: scalar WASM roughly ties the JS JIT, but a
118
- branchless `f32x4` kernel does four values per instruction. Below, a field of a few
119
- thousand particles is held on a grid by springs and pushed aside by your pointer —
120
- every particle's position updated four at a time in WASM SIMD. **Click the button to
121
- toggle WASM ↔ JavaScript** and watch the per-step time — the same code, one path
122
- compiled to SIMD bytecode:
123
-
124
- ```tjs
125
- // One physics step for the whole field, 4 particles per iteration with f32x4 SIMD:
126
- // a spring pulls each particle to its home cell, and an inverse-square push shoves
127
- // it away from the pointer. Pure arithmetic — no per-lane branches (tjs's f32x4 has
128
- // no compare/select yet), which is exactly what SIMD wants. `fallback { }` is the
129
- // scalar-JS twin.
130
- function step(! px: Float32Array, py: Float32Array, vx: Float32Array, vy: Float32Array, hx: Float32Array, hy: Float32Array, n: 0, tx: 0.0, ty: 0.0) {
131
- wasm {
132
- let txv = f32x4_splat(tx)
133
- let tyv = f32x4_splat(ty)
134
- let spring = f32x4_splat(0.03)
135
- let damp = f32x4_splat(0.86)
136
- let repel = f32x4_splat(1600.0)
137
- let soft = f32x4_splat(140.0)
138
- for (let i = 0; i < n; i = i + 4) {
139
- let off = i * 4
140
- let x = f32x4_load(px, off)
141
- let y = f32x4_load(py, off)
142
- let sx = f32x4_mul(f32x4_sub(f32x4_load(hx, off), x), spring)
143
- let sy = f32x4_mul(f32x4_sub(f32x4_load(hy, off), y), spring)
144
- let rx = f32x4_sub(x, txv)
145
- let ry = f32x4_sub(y, tyv)
146
- let push = f32x4_div(repel, f32x4_add(f32x4_add(f32x4_mul(rx, rx), f32x4_mul(ry, ry)), soft))
147
- let nvx = f32x4_mul(f32x4_add(f32x4_load(vx, off), f32x4_add(sx, f32x4_mul(rx, push))), damp)
148
- let nvy = f32x4_mul(f32x4_add(f32x4_load(vy, off), f32x4_add(sy, f32x4_mul(ry, push))), damp)
149
- f32x4_store(vx, off, nvx)
150
- f32x4_store(vy, off, nvy)
151
- f32x4_store(px, off, f32x4_add(x, nvx))
152
- f32x4_store(py, off, f32x4_add(y, nvy))
153
- }
154
- } fallback {
155
- for (let i = 0; i < n; i++) {
156
- const sx = (hx[i] - px[i]) * 0.03, sy = (hy[i] - py[i]) * 0.03
157
- const rx = px[i] - tx, ry = py[i] - ty
158
- const push = 1600.0 / (rx * rx + ry * ry + 140.0)
159
- const nvx = (vx[i] + sx + rx * push) * 0.86, nvy = (vy[i] + sy + ry * push) * 0.86
160
- vx[i] = nvx; vy[i] = nvy; px[i] += nvx; py[i] += nvy
161
- }
162
- }
163
- }
164
-
165
- const W = 480, H = 300, cols = 100, rows = 60, N = cols * rows
166
- const px = new Float32Array(N), py = new Float32Array(N)
167
- const vx = new Float32Array(N), vy = new Float32Array(N)
168
- const hx = new Float32Array(N), hy = new Float32Array(N)
169
- for (let i = 0; i < N; i++) {
170
- const c = i % cols, r = (i / cols) | 0
171
- hx[i] = (c + 0.5) * W / cols
172
- hy[i] = (r + 0.5) * H / rows
173
- px[i] = hx[i]
174
- py[i] = hy[i]
175
- }
176
-
177
- const canvas = document.createElement('canvas')
178
- canvas.width = W
179
- canvas.height = H
180
- canvas.style.cssText = 'width:100%;max-width:480px;border-radius:8px;display:block;background:#0b0e14;touch-action:none;cursor:crosshair'
181
- const ctx = canvas.getContext('2d')
182
-
183
- let tx = W / 2, ty = H / 2, idle = 0, frame = 0
184
- canvas.addEventListener('pointermove', (e) => {
185
- const r = canvas.getBoundingClientRect()
186
- tx = (e.clientX - r.left) / r.width * W
187
- ty = (e.clientY - r.top) / r.height * H
188
- idle = 0
189
- })
190
-
191
- // tjs registers the compiled function as globalThis.__tjs_wasm_0; nulling it makes
192
- // step() fall to its JS twin. (A public WASM on/off toggle would be tidier — noted
193
- // in TJS-FEEDBACK.md.)
194
- let useWasm = true, savedWasm = null
195
- const btn = document.createElement('button')
196
- btn.style.cssText = 'margin:8px 0;padding:6px 12px;border-radius:6px;cursor:pointer'
197
- const readout = document.createElement('p')
198
- function applyMode() {
199
- if (useWasm && savedWasm) { globalThis.__tjs_wasm_0 = savedWasm; savedWasm = null }
200
- if (!useWasm && globalThis.__tjs_wasm_0) { savedWasm = globalThis.__tjs_wasm_0; globalThis.__tjs_wasm_0 = null }
201
- btn.textContent = useWasm ? '⚡ WebAssembly SIMD — click for JavaScript' : '🐢 JavaScript — click for WASM SIMD'
202
- }
203
- btn.onclick = () => { useWasm = !useWasm; applyMode() }
204
-
205
- let acc = 0, frames = 0
206
- function loop() {
207
- if (!document.body.contains(canvas)) return // example removed → stop
208
- frame++
209
- idle++
210
- if (idle > 45) {
211
- tx = W / 2 + Math.cos(frame * 0.02) * W * 0.32
212
- ty = H / 2 + Math.sin(frame * 0.031) * H * 0.32
213
- }
214
- const t0 = performance.now()
215
- step(px, py, vx, vy, hx, hy, N, tx, ty)
216
- acc += performance.now() - t0
217
- frames++
218
- ctx.fillStyle = 'rgba(11,14,20,0.3)'
219
- ctx.fillRect(0, 0, W, H)
220
- ctx.fillStyle = useWasm ? '#4fd1c5' : '#f6ad55'
221
- for (let i = 0; i < N; i++) ctx.fillRect(px[i], py[i], 1.6, 1.6)
222
- if (frames >= 20) {
223
- readout.textContent = N.toLocaleString() + ' particles · ' + (acc / frames).toFixed(3) +
224
- ' ms/step (' + (useWasm ? 'WASM SIMD' : 'JS') + ') · move the pointer'
225
- acc = 0
226
- frames = 0
227
- }
228
- requestAnimationFrame(loop)
229
- }
230
-
231
- ;(async () => {
232
- // The wasm bootstrap is async — wait for the compiled function before starting,
233
- // so the first frames run in WASM rather than the fallback.
234
- const t0 = performance.now()
235
- while (!globalThis.__tjs_wasm_0 && performance.now() - t0 < 3000) {
236
- await new Promise((r) => setTimeout(r, 0))
237
- }
238
- applyMode()
239
- requestAnimationFrame(loop)
240
- })()
241
-
242
- preview.append(canvas, btn, readout)
243
- ```
244
-
245
79
  ## CSS Isolation with `iframe`
246
80
 
247
- Add the `iframe` attribute to render the preview inside an iframe, giving the example
248
- its own document, CSS scope, and custom-element registry. Reach for it when a demo's
249
- styles would otherwise leak into (or get leaked on by) the rest of the page.
250
-
251
- It isolates **DOM and CSS, not state.** The `tosijs`/`tosijs-ui` modules injected
252
- into the iframe are the host page's own instances, so `tosi()` singletons remain
253
- shared across every example (see "How examples run" above). Namespace your state to
254
- keep examples from stomping each other; use `iframe` when you need visual/DOM
255
- isolation.
256
-
257
- *Fully-isolated examples (a separate module realm, so even `tosi()` state and
258
- imported dependencies are sandboxed — the way tjs-lang's playgrounds do it with a
259
- service worker intercepting imports) are a possible future option. For **actual
260
- examples** the shared-page default is usually the nicer behavior: it shows off tosi's
261
- isolation and lets you poke demos through the live global state.*
81
+ Add the `iframe` attribute to render the preview inside an iframe for complete CSS isolation.
262
82
 
263
83
  ## Test Blocks
264
84
 
@@ -432,14 +252,14 @@ export class LiveExample extends Component {
432
252
  const { editors } = this.parts;
433
253
  return [...editors.children].find((elt) => elt.getAttribute('hidden') === null);
434
254
  }
435
- get hydrated() {
436
- try {
437
- return this.parts.js !== undefined;
438
- }
439
- catch {
440
- return false;
441
- }
442
- }
255
+ // Hydration state is the base class's `this.hydrated` (tosijs 1.6.9+). This used to
256
+ // be `try { return this.parts.js !== undefined } catch { return false }` — a probe
257
+ // that, before 1.6.9, PERMANENTLY poisoned the `parts` proxy on the pre-hydration
258
+ // read it performs (rooting it at the light-DOM element, so every later `parts.*`
259
+ // threw — it could blank the whole doc page). It survived only by being light-DOM.
260
+ // 1.6.9 invalidates the proxy at hydrate AND exposes a public `get hydrated()`, so
261
+ // the probe is gone and the private override that collided with it (TS2415) with it.
262
+ // (tosijs#13.)
443
263
  getEditorValue(which) {
444
264
  if (!this.hydrated)
445
265
  return this.pendingValues[which] ?? '';
@@ -598,11 +418,8 @@ export class LiveExample extends Component {
598
418
  this.productTabsReady = true;
599
419
  const { editors } = this.parts;
600
420
  // Relabel the source tab from "js" to the actual dialect (the `part` stays
601
- // `js`, so everything referencing this.parts.js / this.js is unaffected), and
602
- // put the source editor in the dialect's mode — so a `tjs` example gets
603
- // first-class tjs editing (highlighting + autocomplete), `ts` gets TypeScript.
421
+ // `js`, so everything referencing this.parts.js / this.js is unaffected).
604
422
  this.parts.js.setAttribute('name', this.dialect);
605
- this.parts.js.mode = this.dialect;
606
423
  this.jsOutEditor = codeEditor({
607
424
  name: 'JS',
608
425
  mode: 'javascript',
@@ -622,9 +439,10 @@ export class LiveExample extends Component {
622
439
  updateUndo = () => {
623
440
  const { activeTab } = this;
624
441
  const { undo, redo } = this.parts;
625
- if (activeTab instanceof CodeEditor) {
626
- undo.disabled = !activeTab.canUndo();
627
- redo.disabled = !activeTab.canRedo();
442
+ if (activeTab instanceof CodeEditor && activeTab.editor !== undefined) {
443
+ const undoManager = activeTab.editor.session.getUndoManager();
444
+ undo.disabled = !undoManager.hasUndo();
445
+ redo.disabled = !undoManager.hasRedo();
628
446
  }
629
447
  else {
630
448
  undo.disabled = true;
@@ -647,13 +465,13 @@ export class LiveExample extends Component {
647
465
  undo = () => {
648
466
  const { activeTab } = this;
649
467
  if (activeTab instanceof CodeEditor) {
650
- activeTab.undo();
468
+ activeTab.editor.undo();
651
469
  }
652
470
  };
653
471
  redo = () => {
654
472
  const { activeTab } = this;
655
473
  if (activeTab instanceof CodeEditor) {
656
- activeTab.redo();
474
+ activeTab.editor.redo();
657
475
  }
658
476
  };
659
477
  get isMaximized() {
@@ -943,11 +761,15 @@ export class LiveExample extends Component {
943
761
  }
944
762
  canUndo() {
945
763
  const t = this.activeTab;
946
- return t instanceof CodeEditor && t.canUndo();
764
+ return (t instanceof CodeEditor &&
765
+ t.editor !== undefined &&
766
+ t.editor.session.getUndoManager().hasUndo());
947
767
  }
948
768
  canRedo() {
949
769
  const t = this.activeTab;
950
- return t instanceof CodeEditor && t.canRedo();
770
+ return (t instanceof CodeEditor &&
771
+ t.editor !== undefined &&
772
+ t.editor.session.getUndoManager().hasRedo());
951
773
  }
952
774
  saveLocalEdit = () => {
953
775
  const key = this.localEditKey();