code-playpad 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -3,37 +3,134 @@
3
3
  [![npm](https://img.shields.io/npm/v/code-playpad)](https://www.npmjs.com/package/code-playpad) [![license](https://img.shields.io/npm/l/code-playpad)](./LICENSE)
4
4
 
5
5
  Runnable code in any web page. A CodeMirror editor that **executes what it
6
- shows** — Python, JavaScript and TypeScript — entirely in the reader's browser.
7
- No backend, no iframe, no sign-up.
6
+ shows** — Python, JavaScript, TypeScript and a documented subset of Java —
7
+ entirely in the reader's browser. No backend, no iframe, no sign-up.
8
8
 
9
9
  **[Live demo →](https://code-playpad.swengr.com)**
10
10
 
11
11
  ```html
12
- <script type="module" src="https://cdn.jsdelivr.net/npm/code-playpad@0.2.0"></script>
12
+ <script type="module" src="https://cdn.jsdelivr.net/npm/code-playpad@0.3.0"></script>
13
13
 
14
- <code-playpad height="180">
14
+ <code-playpad height="auto">
15
15
  print("hello from a static page")
16
16
  </code-playpad>
17
17
  ```
18
18
 
19
+ **Contents** — [Languages](#languages) · [Install](#install) · [Attributes](#attributes)
20
+ · [Sizing](#sizing) · [Several files](#several-files) · [Input](#input)
21
+ · [Controlling it from JavaScript](#controlling-it-from-javascript)
22
+ · [Events and grading](#events-and-grading) · [Saving work](#saving-work)
23
+ · [Light and dark](#light-and-dark) · [Styling](#styling) · [Framework recipes](#framework-recipes)
24
+ · [Production](#production) · [Worth knowing](#worth-knowing)
25
+
19
26
  ## Languages
20
27
 
21
28
  | `language` | Runtime | Downloaded at runtime |
22
29
  | --- | --- | --- |
23
- | `python` (default) | Python 3.14 via Pyodide — full standard library, `input()`, real tracebacks, `numpy` on demand | ~11 MB, once, cached |
30
+ | `python` (default) | Python 3.14 via Pyodide — full standard library, `input()`, real tracebacks, `numpy` on demand | ~6 MB (13.5 MB unpacked), once, cached |
24
31
  | `javascript` | The browser's own engine, in a Worker | nothing |
25
- | `typescript` | Same engine, types stripped before running | ~50 KB, once, cached |
32
+ | `typescript` | Same engine, types stripped before running | ~70 KB, once, cached |
33
+ | `java` | A documented subset of Java 25, compiled and interpreted in a Worker by an engine built for this package — see [Java](#java) | nothing: the engine is a ~27 KB (gzipped) chunk of this package, loaded with the first Java widget |
34
+
35
+ ```html
36
+ <code-playpad height="auto">print("python is the default")</code-playpad>
37
+
38
+ <code-playpad language="javascript" height="auto">
39
+ const doubled = [1, 2, 3].map((n) => n * 2);
40
+ console.log(doubled);
41
+ doubled.reduce((a, b) => a + b, 0)
42
+ </code-playpad>
26
43
 
27
- Each language loads **only on pages that use it**. A Python-only page never
28
- downloads a byte of the JavaScript runtime; a JavaScript-only page never touches
29
- Pyodide's 11 MB.
44
+ <code-playpad language="typescript" height="auto">
45
+ type Point = { x: number; y: number };
46
+ const dist = (p: Point): number => Math.hypot(p.x, p.y);
47
+ dist({ x: 3, y: 4 })
48
+ </code-playpad>
49
+ ```
50
+
51
+ A trailing expression prints its value, like a REPL. Each language loads **only
52
+ on pages that use it**: a Python-only page never downloads a byte of the
53
+ JavaScript runtime, and a JavaScript-only page never touches Pyodide's ~6 MB.
30
54
 
31
55
  TypeScript here erases types, it does not check them — a type error will not
32
56
  stop your code running. This is a playground, not `tsc`.
33
57
 
58
+ ### Java
59
+
60
+ ```html
61
+ <code-playpad language="java" height="auto">
62
+ int visits = 0;
63
+
64
+ void main() {
65
+ greet("Ada");
66
+ IO.println("greeted " + visits + " people");
67
+ }
68
+
69
+ void greet(String name) {
70
+ visits++;
71
+ IO.println("Hello, " + name + "!");
72
+ }
73
+ </code-playpad>
74
+ ```
75
+
76
+ Java runs on an engine written for code-playpad, not on a JVM. It covers a
77
+ documented subset of Java 25 and checks its behaviour against a real JDK 25:
78
+
79
+ - Java 25 **compact source files** and instance `main`, or a classic
80
+ `public class Main` with `public static void main(String[] args)`
81
+ - `int`, `char`, `boolean`, `String`; one-dimensional arrays; static and
82
+ instance methods with overloading and recursion; fields; `if`/`while`/`for`,
83
+ `break`/`continue`/`return`; `String.length()`/`charAt()`/`equals()`;
84
+ `System.out`/`System.err` and `IO.print`/`IO.println`
85
+ - **Errors read like the JDK's**: compile errors in javac's format, exceptions
86
+ with Java stack traces and the JDK's NullPointerException messages
87
+
88
+ Anything outside the subset — `long`/`double`, `?:`, `switch`, objects and
89
+ constructors, collections, `Scanner`, imports — is reported as *not supported in
90
+ this version* rather than run with a different meaning. The file is `Main.java`
91
+ unless `entry` says otherwise. The engine stops runaway programs after 200
92
+ million steps and explains why.
93
+
94
+ ## Download size
95
+
96
+ What a page downloads once a reader presses Run, with the CDN build. Measured by
97
+ loading one-widget pages in a browser and gzipping each file the page fetched;
98
+ CDN downloads are what the CDN actually sent.
99
+
100
+ | Page uses | code-playpad (gzip) | Also fetched from a CDN at run time | 0.2.1 (gzip) |
101
+ | --- | --- | --- | --- |
102
+ | Python | **165.8 KB** | Pyodide, about **6.0 MB** (a 3.3 MB wasm file and a 2.4 MB standard library) | 165.2 KB |
103
+ | JavaScript | **179.9 KB** | nothing | 180.5 KB |
104
+ | TypeScript | **179.9 KB** | Sucrase, 72 KB (≈ 252 KB in total) | ≈ 252.6 KB |
105
+ | Java | **189.7 KB** | nothing | n/a |
106
+ | All four | 247.2 KB | Pyodide and Sucrase | n/a |
107
+
108
+ - **A shared base of 144 KB is in every row:** the editor (CodeMirror) and the
109
+ widget, the same as 0.2.1 within 0.6 KB.
110
+ - **Each language adds:** Python 21.7 KB; JavaScript/TypeScript 35.8 KB; Java
111
+ 45.6 KB, which is the 26.5 KB engine plus 19 KB of Java syntax highlighting.
112
+ - **Languages load lazily.** A Java page never fetches the Python or JavaScript
113
+ runtimes, and Python and JavaScript pages never fetch Java, so adding Java left
114
+ Python-only and JavaScript-only pages the same size as in 0.2.1.
115
+ - **Java is the lightest to run:** it is self-contained, while Python's first run
116
+ is dominated by the roughly 6 MB Pyodide download (cached after that).
117
+ - **With npm and a bundler** you already ship CodeMirror yourself. code-playpad's
118
+ own code then costs about 9 KB, plus the runtime chunk for each language used;
119
+ Java's is 27 KB, plus `@codemirror/lang-java` if you install it.
120
+
34
121
  ## Install
35
122
 
36
- The CDN snippet above needs nothing installed. For a bundler:
123
+ ### From a CDN — nothing to install
124
+
125
+ ```html
126
+ <script type="module" src="https://cdn.jsdelivr.net/npm/code-playpad@0.3.0"></script>
127
+ ```
128
+
129
+ This is the `standalone` build: CodeMirror is bundled in, so a static blog post
130
+ or a Markdown-rendered page needs only that one tag. Pin the version — an
131
+ unpinned URL follows `latest` and will change under your readers.
132
+
133
+ ### From npm, with a bundler
37
134
 
38
135
  ```bash
39
136
  npm install code-playpad @codemirror/state @codemirror/view @codemirror/commands \
@@ -41,59 +138,72 @@ npm install code-playpad @codemirror/state @codemirror/view @codemirror/commands
41
138
  ```
42
139
 
43
140
  CodeMirror is a peer dependency so you never end up with two copies of
44
- `@codemirror/state`. Language grammars (`@codemirror/lang-python`,
45
- `lang-javascript`, `lang-json`) are **optional** — install only the ones you
46
- use; a missing one degrades to plain text.
141
+ `@codemirror/state`. Language grammars are **optional** — install only what you
142
+ use (`@codemirror/lang-python`, `@codemirror/lang-javascript`,
143
+ `@codemirror/lang-json`, `@codemirror/lang-java`); a missing one degrades to
144
+ plain text rather than throwing.
47
145
 
48
146
  ```js
49
147
  import 'code-playpad/element'; // registers <code-playpad>
50
148
  ```
51
149
 
52
- Importing for the side effect is enough. If your bundler is configured to drop
53
- side-effect-only imports, register explicitly instead:
150
+ If your bundler is configured to drop side-effect-only imports, register
151
+ explicitly instead:
54
152
 
55
153
  ```js
56
154
  import { defineCodePlayPad } from 'code-playpad/element';
57
155
  defineCodePlayPad();
58
156
  ```
59
157
 
60
- From React:
158
+ ### Entry points
61
159
 
62
- ```jsx
63
- import { CodePlayPad } from 'code-playpad/react';
64
-
65
- <CodePlayPad
66
- language="python"
67
- code={'print("hi")'}
68
- height={180}
69
- onOutput={(r) => setPassed(r.stdout.includes('hi'))}
70
- />
71
- ```
160
+ | Import | You get |
161
+ | --- | --- |
162
+ | `code-playpad/element` | registers `<code-playpad>`; smallest thing that works |
163
+ | `code-playpad` | the same, plus every helper below |
164
+ | `code-playpad/react` | the `<CodePlayPad>` React component |
165
+ | `code-playpad/standalone` | CodeMirror bundled in — what the CDN serves |
72
166
 
73
- Importing on a server is safe — the element degrades to a no-op without a DOM,
74
- so Next.js, Astro and SvelteKit need no dynamic import.
167
+ Importing on a server is safe: without a DOM the element degrades to a no-op, so
168
+ Next.js, Astro and SvelteKit need no dynamic import or `client:only` guard.
75
169
 
76
- ## API
170
+ ## Attributes
77
171
 
78
172
  | | |
79
173
  | --- | --- |
80
- | **attributes** | `language`, `height`, `min-height`, `max-height`, `theme` (`light` \| `dark` \| `auto`), `readonly`, `autorun`, `stdin`, `persist`, `entry` |
81
- | **properties** | `code`, `files`, `entry`, `stdin`, `language` |
82
- | **methods** | `run()`, `stop()`, `reset()`, `getCode()`, `getFiles()`, `openFile(name)`, `prewarm()` |
83
- | **events** | `py-ready`, `py-run`, `py-output`, `py-error` |
174
+ | `language` | `python` (default), `javascript`, `typescript`, `java` |
175
+ | `height` | number (px), any CSS length, or `auto` |
176
+ | `min-height` / `max-height` | bounds for `auto` |
177
+ | `theme` | `light`, `dark`, or `auto` (default — follows the OS) |
178
+ | `readonly` | editor cannot be typed into; Run still works |
179
+ | `autorun` | runs once, on its own, when scrolled into view |
180
+ | `stdin` | shows the standard-input box up front |
181
+ | `persist` | key under which edits are saved (see [Saving work](#saving-work)) |
182
+ | `entry` | which file Run executes, for multi-file widgets |
183
+
184
+ ```html
185
+ <!-- a worked example nobody should edit -->
186
+ <code-playpad height="auto" readonly>
187
+ print("read this, don't change it")
188
+ </code-playpad>
84
189
 
85
- Events bubble and cross the shadow boundary, so answer-checking lives outside
86
- the widget:
190
+ <!-- output visible before the reader does anything -->
191
+ <code-playpad height="auto" autorun>
192
+ print("this already ran")
193
+ </code-playpad>
87
194
 
88
- ```js
89
- document.addEventListener('py-output', (e) => {
90
- // e.detail: { stdout, stderr?, value?, table?, durationMs, language }
91
- grade(e.target.id, e.detail.stdout);
92
- });
195
+ <!-- always dark, regardless of the reader's OS setting -->
196
+ <code-playpad height="auto" theme="dark">
197
+ print("always dark")
198
+ </code-playpad>
93
199
  ```
94
200
 
95
- Note `language`, not `lang` — `lang` is a standard HTML attribute for the
96
- *human* language of an element's text.
201
+ `readonly` blocks typing but not execution, and the code can still be replaced
202
+ from JavaScript — useful for "show the answer" buttons.
203
+
204
+ Note it is `language`, not `lang`: `lang` is a standard HTML attribute for the
205
+ *human* language of an element's text, and putting `python` there confuses
206
+ screen readers.
97
207
 
98
208
  ## Sizing
99
209
 
@@ -125,11 +235,11 @@ length (`40vh`, `30em`).
125
235
 
126
236
  ## Several files
127
237
 
128
- A widget can hold a small project. Relative imports, packages, and data files
129
- all work, and tracebacks name the real file and line.
238
+ A widget can hold a small project. Relative imports, packages and data files all
239
+ work, and tracebacks name the real file and line.
130
240
 
131
241
  ```html
132
- <code-playpad entry="main.py" height="260">
242
+ <code-playpad entry="main.py" height="auto" max-height="420">
133
243
  <playpad-file name="main.py">
134
244
  from geometry import Circle
135
245
  print(Circle(1).area())
@@ -149,9 +259,34 @@ all work, and tracebacks name the real file and line.
149
259
  </code-playpad>
150
260
  ```
151
261
 
262
+ JavaScript and TypeScript work the same way, including `.json` imported as data:
263
+
264
+ ```html
265
+ <code-playpad language="javascript" entry="main.js" height="auto">
266
+ <playpad-file name="main.js">
267
+ import { total } from "./cart.js";
268
+ import items from "./items.json";
269
+
270
+ console.log(total(items));
271
+ </playpad-file>
272
+
273
+ <playpad-file name="cart.js">
274
+ export const total = (items) =>
275
+ items.reduce((sum, item) => sum + item.price, 0);
276
+ </playpad-file>
277
+
278
+ <playpad-file name="items.json">
279
+ [{ "price": 12 }, { "price": 30 }]
280
+ </playpad-file>
281
+ </code-playpad>
282
+ ```
283
+
284
+ A tab bar appears once there is more than one file, with `▸` marking the entry
285
+ point, and each file keeps its own undo history. Editing a module takes effect
286
+ on the next run — the import cache is cleared between runs.
287
+
152
288
  Highlighting follows the **file**, not the runtime: a `.json` beside your
153
289
  JavaScript is highlighted as JSON, a `.csv` beside your Python is plain text.
154
- `.json` files are importable as data, the same as in Node.
155
290
 
156
291
  ## Input
157
292
 
@@ -165,10 +300,299 @@ age? 36
165
300
  hello Ada, next year you turn 37
166
301
  ```
167
302
 
168
- `Enter` sends a line; **EOF** (or `Ctrl-D`) ends input. In JavaScript and
169
- TypeScript, `input()` is a global returning a promise, so `await` works at the
170
- top level. Echoed input is display-only — `Result.stdout` stays exactly what the
171
- program printed, so graders are unaffected.
303
+ `Enter` sends a line; **EOF** (or `Ctrl-D`) ends input, which is how you finish
304
+ a `while True:` loop.
305
+
306
+ In JavaScript and TypeScript, `input()` is a global returning a promise, so
307
+ `await` works at the top level:
308
+
309
+ ```html
310
+ <code-playpad language="javascript" height="auto">
311
+ const name = await input("your name? ");
312
+ console.log(`hello ${name}`);
313
+ </code-playpad>
314
+ ```
315
+
316
+ To script the answers instead of typing them — useful for a worked example —
317
+ pre-fill the box. Those lines are used before the reader is asked:
318
+
319
+ ```html
320
+ <code-playpad height="auto" stdin>
321
+ name = input("name? ")
322
+ print("hi", name)
323
+ </code-playpad>
324
+ ```
325
+
326
+ ```js
327
+ document.querySelector('code-playpad').stdin = 'Ada\n36';
328
+ ```
329
+
330
+ Echoed input is display-only: `stdout` stays exactly what the program printed,
331
+ so graders are unaffected.
332
+
333
+ Java does not read input yet; `Scanner` over the input box is planned.
334
+
335
+ ## Controlling it from JavaScript
336
+
337
+ ```js
338
+ const pad = document.querySelector('code-playpad');
339
+
340
+ pad.code = 'print("set from outside")'; // replace the visible file
341
+ const source = pad.getCode(); // read it back
342
+ const result = await pad.run(); // { stdout, stderr?, value?, durationMs }
343
+ pad.reset(); // back to the snippet the page shipped
344
+ pad.stop(); // stop this widget's run, queued or running
345
+ await pad.prewarm(); // boot the runtime before the reader runs anything
346
+ ```
347
+
348
+ Multi-file widgets add:
349
+
350
+ ```js
351
+ pad.files = { // replace the whole project
352
+ 'main.py': 'from util import shout\nprint(shout("hi"))',
353
+ 'util.py': 'def shout(s): return s.upper() + "!"'
354
+ };
355
+ pad.entry = 'main.py'; // which file Run executes
356
+ pad.openFile('util.py'); // bring a tab on screen
357
+ const everything = pad.getFiles(); // { name: contents }
358
+ ```
359
+
360
+ `prewarm()` is worth calling when you know the reader is about to run something
361
+ — on a lesson page, for instance — so the ~6 MB Python download starts before
362
+ they press Run rather than after.
363
+
364
+ ## Events and grading
365
+
366
+ Four events bubble and cross the shadow boundary, so answer-checking lives
367
+ outside the widget rather than inside it:
368
+
369
+ | event | `event.detail` |
370
+ | --- | --- |
371
+ | `py-ready` | `{ language, label }` — runtime booted |
372
+ | `py-run` | `{ code, entry, files, language }` — a run started |
373
+ | `py-output` | `{ stdout, stderr?, value?, table?, durationMs, language }` |
374
+ | `py-error` | `{ message, hint?, language }` |
375
+
376
+ A complete exercise check:
377
+
378
+ ```html
379
+ <code-playpad id="ex1" height="auto" min-height="150" persist="ex1">
380
+ # Print the numbers 1 to 5, one per line.
381
+ </code-playpad>
382
+ <p id="ex1-result"></p>
383
+ ```
384
+
385
+ ```js
386
+ const expected = '1\n2\n3\n4\n5\n';
387
+
388
+ document.addEventListener('py-output', (event) => {
389
+ if (event.target.id !== 'ex1') return;
390
+
391
+ const el = document.getElementById('ex1-result');
392
+ if (event.detail.stderr) {
393
+ el.textContent = 'Your code raised an error — read the message above.';
394
+ } else if (event.detail.stdout === expected) {
395
+ el.textContent = '✅ Correct';
396
+ } else {
397
+ el.textContent = `❌ Expected ${JSON.stringify(expected)}, got ${JSON.stringify(event.detail.stdout)}`;
398
+ }
399
+ });
400
+ ```
401
+
402
+ `py-error` also carries a `hint` when the runtime can explain the failure in
403
+ plain language — for example when a program calls `input()` more times than the
404
+ standard-input box has lines.
405
+
406
+ ## Saving work
407
+
408
+ `persist` stores the whole file map in IndexedDB, so a reader's edits survive
409
+ navigation *and* a full reload:
410
+
411
+ ```html
412
+ <code-playpad height="auto" persist="lesson-3-exercise-1">
413
+ # Your code here
414
+ </code-playpad>
415
+ ```
416
+
417
+ The page's file list stays authoritative: edits come back, but files you add or
418
+ remove later follow the page, not the old snapshot.
419
+
420
+ For a course you usually also want completion state, which the package exports:
421
+
422
+ ```js
423
+ import { setProgress, getProgress, allProgress, loadSnippet, clearSnippet } from 'code-playpad';
424
+
425
+ // mark a lesson done when its exercise passes
426
+ document.addEventListener('py-output', async (event) => {
427
+ if (event.detail.stdout === expected) {
428
+ await setProgress('lesson-3', { completed: true, score: 100 });
429
+ }
430
+ });
431
+
432
+ await getProgress('lesson-3'); // { id, completed, score, updatedAt }
433
+ await allProgress(); // every record, for a progress bar
434
+ await loadSnippet('lesson-3-exercise-1'); // what the reader last wrote
435
+ await clearSnippet('lesson-3-exercise-1'); // a "start over" button
436
+ ```
437
+
438
+ All of it is per-browser: nothing leaves the reader's machine.
439
+
440
+ ## Light and dark
441
+
442
+ `theme` takes `light`, `dark`, or `auto` (the default).
443
+
444
+ ```html
445
+ <code-playpad theme="dark" height="auto">print("always dark")</code-playpad>
446
+ <code-playpad theme="light" height="auto">print("always light")</code-playpad>
447
+ <code-playpad height="auto">print("follows the page, then the OS")</code-playpad>
448
+ ```
449
+
450
+ **`auto` follows your site first, the operating system second.** If your page
451
+ has its own light/dark switch, the widget follows it as long as the switch sets
452
+ `color-scheme` on the root element — which is how most themes are written:
453
+
454
+ ```css
455
+ :root { color-scheme: light; }
456
+ :root[data-theme="dark"] { color-scheme: dark; }
457
+ ```
458
+
459
+ Flip `data-theme` (or a class, or an inline style) and every widget on the page
460
+ re-themes itself. Nothing else to wire up. With no `color-scheme` on the page,
461
+ `auto` falls back to the reader's OS setting via `prefers-color-scheme`.
462
+
463
+ If your theme switch does *not* set `color-scheme`, drive the widgets directly:
464
+
465
+ ```js
466
+ function setTheme(mode) { // mode: 'light' | 'dark'
467
+ document.documentElement.dataset.theme = mode;
468
+ for (const pad of document.querySelectorAll('code-playpad')) {
469
+ pad.setAttribute('theme', mode);
470
+ }
471
+ }
472
+ ```
473
+
474
+ An explicit `theme` attribute always wins over the page, so a deliberately dark
475
+ snippet stays dark on a light page.
476
+
477
+ ## Styling
478
+
479
+ The widget lives in a shadow root, so page CSS cannot leak in — and its own
480
+ styles cannot leak out. Two supported ways to restyle it.
481
+
482
+ **Custom properties** inherit through the shadow boundary:
483
+
484
+ ```css
485
+ code-playpad {
486
+ --cp-accent: #b5179e; /* buttons, focus, the caret */
487
+ --cp-radius: 14px;
488
+ --cp-font-mono: "JetBrains Mono", monospace;
489
+ }
490
+ ```
491
+
492
+ | | |
493
+ | --- | --- |
494
+ | colours | `--cp-bg`, `--cp-fg`, `--cp-muted`, `--cp-border`, `--cp-surface` |
495
+ | accents | `--cp-accent`, `--cp-accent-fg`, `--cp-danger`, `--cp-ok` |
496
+ | shape and type | `--cp-radius`, `--cp-font-mono`, `--cp-font-ui` |
497
+
498
+ **Parts** expose the pieces worth targeting:
499
+
500
+ ```css
501
+ code-playpad::part(run-button) { border-radius: 999px; }
502
+ code-playpad::part(stop-button) { font-weight: 700; }
503
+ code-playpad::part(output) { background: #fff7fb; }
504
+ code-playpad::part(tabs) { border-bottom-width: 2px; }
505
+ ```
506
+
507
+ Both work from an ordinary stylesheet, no `::shadow` hacks. For dark mode, the
508
+ `theme` attribute already follows the reader's OS by default.
509
+
510
+ ## Framework recipes
511
+
512
+ **Astro** — put the import in a page or layout script; the element does the rest.
513
+
514
+ ```astro
515
+ ---
516
+ // src/layouts/Lesson.astro
517
+ ---
518
+ <script>
519
+ import 'code-playpad/element';
520
+ </script>
521
+
522
+ <code-playpad height="auto" persist={Astro.props.id}>
523
+ {Astro.props.code}
524
+ </code-playpad>
525
+ ```
526
+
527
+ **React** — the wrapper assigns props through a ref, so React 18 behaves like 19.
528
+
529
+ ```jsx
530
+ import { useState } from 'react';
531
+ import { CodePlayPad } from 'code-playpad/react';
532
+
533
+ export function Exercise() {
534
+ const [passed, setPassed] = useState(false);
535
+
536
+ return (
537
+ <>
538
+ <CodePlayPad
539
+ language="python"
540
+ code={'# Your code here'}
541
+ height="auto"
542
+ minHeight={150}
543
+ persist="exercise-1"
544
+ onOutput={(r) => setPassed(r.stdout.trim() === '42')}
545
+ />
546
+ {passed && <p>✅ Correct</p>}
547
+ </>
548
+ );
549
+ }
550
+ ```
551
+
552
+ Props: `code`, `files`, `entry`, `language`, `height`, `stdin`, `theme`,
553
+ `readOnly`, `autoRun`, `persist`, `className`, `style`, and the callbacks
554
+ `onReady`, `onRun`, `onOutput`, `onError`.
555
+
556
+ **Next.js** — the element is SSR-safe, so a plain import works in a client
557
+ component:
558
+
559
+ ```jsx
560
+ 'use client';
561
+ import 'code-playpad/element';
562
+
563
+ export default function Lesson() {
564
+ return <code-playpad height="auto">{'print("hi")'}</code-playpad>;
565
+ }
566
+ ```
567
+
568
+ **Vue** — tell the compiler the tag is a custom element, then use it directly:
569
+
570
+ ```js
571
+ // vite.config.js
572
+ export default {
573
+ plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'code-playpad' } } })]
574
+ };
575
+ ```
576
+
577
+ **Plain HTML / Markdown** — the CDN tag plus the element. Because the code lives
578
+ in the element's text content, the snippet is still readable if JavaScript never
579
+ loads.
580
+
581
+ ## Production
582
+
583
+ Serve Pyodide from your own origin so you control its cache headers, rather than
584
+ depending on a public CDN:
585
+
586
+ ```js
587
+ import { configurePython, pyodideAssetUrls } from 'code-playpad';
588
+
589
+ configurePython({ indexURL: '/pyodide/' }); // before the first run
590
+ pyodideAssetUrls(); // the files worth <link rel=preload>-ing
591
+ ```
592
+
593
+ `configureTypeScript({ sucraseURL })` does the same for the TypeScript
594
+ transform. Give those files a long `Cache-Control`: one page's download then
595
+ warms every other page on the site.
172
596
 
173
597
  ## Worth knowing
174
598
 
@@ -177,15 +601,93 @@ program printed, so graders are unaffected.
177
601
  single-threaded, so a second widget waits for the first.
178
602
  - **Runaway code cannot freeze the page.** Code runs in a Worker; **Stop**
179
603
  terminates and reboots it. Runs queued behind the stopped one survive.
604
+ Stopping a widget that is still `Queued…` just takes it out of the queue —
605
+ whatever is running keeps running.
180
606
  - **A program waiting on `input()` holds the shared runtime.** Other widgets
181
607
  show `Queued…` until it finishes or is stopped.
182
608
  - **Nothing boots on page load.** An IntersectionObserver starts a runtime as a
183
- widget nears the viewport; touching the editor also wakes it.
609
+ widget nears the viewport; touching the editor also wakes it. Use `prewarm()`
610
+ to start earlier on purpose.
184
611
  - **Python in the browser has limits**: no threads, no `multiprocessing`, no
185
612
  sockets, no `subprocess`. `requests` works but goes through browser `fetch`,
186
613
  so CORS applies.
187
- - **Serve Pyodide from your own origin in production** so you control its cache
188
- headers: `configurePython({ indexURL: '/pyodide/' })` before the first run.
614
+ - **If CodeMirror fails to load**, the widget falls back to a plain textarea that
615
+ still runs code, rather than showing nothing.
616
+
617
+ ## Version history
618
+
619
+ No breaking changes so far: markup and API written against 0.1.3 still work on
620
+ 0.3.0.
621
+
622
+ ### 0.3.0
623
+
624
+ - **`language="java"`**: a documented subset of Java 25 — see [Java](#java).
625
+ The engine is a ~27 KB (gzipped) chunk loaded only by pages with a Java
626
+ widget; Python and JavaScript pages download the same as in 0.2.1 (see
627
+ [Download size](#download-size)). `@codemirror/lang-java` is a new optional
628
+ peer dependency for its highlighting.
629
+ - **Fixed:** in JavaScript, a top-level `await` inside an expression
630
+ (`Number(await input())`) failed with `SyntaxError: missing ) after argument
631
+ list`; it only worked when the first `await` started a statement.
632
+ - A hint about something other than input (a runaway loop, say) no longer opens
633
+ the standard-input box; only "input() asked for a line…" does.
634
+
635
+ 0.2.2 was never published; its fixes ship in 0.3.0:
636
+
637
+ - **Fixed:** pressing **Stop** on a widget still showing `Queued…` stopped a
638
+ *different* widget — whichever one was running — and the queued run then
639
+ went ahead anyway. Stop now cancels only the widget's own run: a queued run is
640
+ dropped without touching the worker, and only a run that is actually
641
+ executing kills and reboots it. `pad.stop()` on an idle widget no longer
642
+ affects anyone else.
643
+ - **Fixed:** stopping a run while the runtime was rebooting from an earlier
644
+ Stop could wedge the queue — every later run sat at `Queued…` forever.
645
+ - `Runtime.stop()` takes an optional argument: the promise `run()` returned,
646
+ to stop just that run. With no argument it behaves as before.
647
+ - **Fixed:** a line printed just before an infinite loop (`print("started")`
648
+ then `while True: pass`) never appeared — output batching held it back, and
649
+ the loop never let the worker send it. The first lines of any burst are now
650
+ sent immediately, in Python and JavaScript; large outputs are still batched.
651
+ - The JavaScript/TypeScript worker is now minified in `dist/` like the Python
652
+ one; it previously shipped as readable source, comments included. The
653
+ `javascript` chunk drops from about 12KB to about 7KB.
654
+
655
+ ### 0.2.1
656
+
657
+ - `theme="auto"` now follows the **page** before the operating system: if your
658
+ site's light/dark switch sets `color-scheme` on the root, widgets re-theme
659
+ with it. Previously a page could go dark while every widget stayed light.
660
+ - `minHeight` and `maxHeight` props on the React wrapper — 0.2.0 added them as
661
+ attributes but missed the wrapper.
662
+ - README expanded into full usage documentation: entry points, an example per
663
+ attribute, programmatic control, a complete grading example, the progress
664
+ store, styling, and framework recipes.
665
+
666
+ ### 0.2.0
667
+
668
+ - `height="auto"` is a supported content-fitting mode. It previously worked by
669
+ accident and could have broken silently.
670
+ - New `min-height` and `max-height` attributes. `max-height` scrolls rather than
671
+ clipping; `min-height` grows the editor itself, so the empty space below a
672
+ short snippet is still clickable — what an exercise answer box needs.
673
+
674
+ ### 0.1.4
675
+
676
+ - **Fixed:** a `<pre>` or other wrapper on its own line leaked its indentation
677
+ into the program, so two-line snippets failed with `IndentationError` while
678
+ single-line ones looked fine.
679
+ - **Fixed:** `sideEffects` named the re-export shims rather than the chunk that
680
+ registers the element, so a bundler could legitimately drop it and
681
+ `<code-playpad>` would never register in a production build.
682
+ - No runtime dependencies (an analytics package used only by the demo had crept
683
+ into `dependencies`).
684
+
685
+ ### 0.1.3
686
+
687
+ - Relicensed **MIT**. Earlier versions were published as `UNLICENSED`, which
688
+ granted nobody the right to use them.
689
+
690
+ Versions 0.1.0 to 0.1.2 were withdrawn and are not installable.
189
691
 
190
692
  ## Licence
191
693
 
@@ -196,3 +698,8 @@ it, modify it. Bundled third-party components (CodeMirror 6, Lezer) remain under
196
698
  their own MIT licences, reproduced in full in `dist/THIRD-PARTY-NOTICES.txt`.
197
699
  Pyodide (MPL-2.0) is fetched from a CDN at runtime and is not redistributed by
198
700
  this package.
701
+
702
+ The Java engine is original code under the same MIT licence. Java is a trademark
703
+ of Oracle and/or its affiliates; code-playpad is not affiliated with, endorsed
704
+ by, or certified by Oracle, and its Java support is a documented subset, not a
705
+ Java implementation.