tosijs-ui 1.6.20 → 1.6.21

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.
@@ -61,7 +61,7 @@ export async function executeCode(code, context, transform) {
61
61
  // self-contained) and is loaded only for `ts` examples; from-ts in turn fetches
62
62
  // the TypeScript compiler lazily at runtime, so tsc is never pulled in until a TS
63
63
  // example actually transforms. Pinned to match the dev dep.
64
- const TJS_VERSION = '0.8.7';
64
+ const TJS_VERSION = '0.9.0';
65
65
  // Where to fetch a tjs-lang browser bundle from, in priority order:
66
66
  // 1. SAME-ORIGIN — the doc-site build copies the bundles next to the iife and
67
67
  // sets `__TJS_LOCAL_BASE`, so the transpiler ships in lockstep with the page,
@@ -33,6 +33,38 @@ 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
+
36
68
  ## Source dialects: `js`, `tjs`, `ts`
37
69
 
38
70
  The executable block's fence language picks how the source is compiled before it
@@ -76,9 +108,157 @@ example the equivalent is written inside a comment (so it survives `tsc`) — bu
76
108
  that comment form can't appear inside a doc comment like this one, since the
77
109
  test's own closing delimiter would end the doc comment.
78
110
 
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
+
79
245
  ## CSS Isolation with `iframe`
80
246
 
81
- Add the `iframe` attribute to render the preview inside an iframe for complete CSS isolation.
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.*
82
262
 
83
263
  ## Test Blocks
84
264
 
@@ -418,8 +598,11 @@ export class LiveExample extends Component {
418
598
  this.productTabsReady = true;
419
599
  const { editors } = this.parts;
420
600
  // Relabel the source tab from "js" to the actual dialect (the `part` stays
421
- // `js`, so everything referencing this.parts.js / this.js is unaffected).
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.
422
604
  this.parts.js.setAttribute('name', this.dialect);
605
+ this.parts.js.mode = this.dialect;
423
606
  this.jsOutEditor = codeEditor({
424
607
  name: 'JS',
425
608
  mode: 'javascript',