@ox-content/code-play 3.0.0-alpha.1 → 3.0.0-alpha.10

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/dist/browser.mjs CHANGED
@@ -692,10 +692,115 @@ function buildJavaScriptSandboxDocument(code, messageId) {
692
692
  })();
693
693
  <\/script></body></html>`;
694
694
  }
695
+ function buildJavaScriptWorkerSource() {
696
+ return `
697
+ (function () {
698
+ function format(args) {
699
+ return Array.prototype.map.call(args, function (value) {
700
+ if (typeof value === "string") return value;
701
+ if (value === undefined) return "undefined";
702
+ if (value === null) return "null";
703
+ try { return JSON.stringify(value); } catch (error) { return String(value); }
704
+ }).join(" ") + "\\n";
705
+ }
706
+ self.onmessage = function (event) {
707
+ var data = event.data || {};
708
+ if (!data.id) return;
709
+ var stdout = [];
710
+ var stderr = [];
711
+ var consoleLike = {
712
+ log: function () { stdout.push(format(arguments)); },
713
+ info: function () { stdout.push(format(arguments)); },
714
+ warn: function () { stderr.push(format(arguments)); },
715
+ error: function () { stderr.push(format(arguments)); }
716
+ };
717
+ try {
718
+ var run = new Function("console", '"use strict";\\n' + String(data.code || ""));
719
+ var value = run(consoleLike);
720
+ self.postMessage({
721
+ id: data.id,
722
+ stdout: stdout,
723
+ stderr: stderr,
724
+ value: value === undefined ? undefined : String(value)
725
+ });
726
+ } catch (error) {
727
+ var message = error && error.message ? String(error.message) : String(error);
728
+ self.postMessage({ id: data.id, stdout: stdout, stderr: stderr, error: message });
729
+ }
730
+ };
731
+ })();
732
+ `;
733
+ }
695
734
  function applySandboxStreams(stdio, message) {
696
735
  for (const text of message.stdout ?? []) stdio.push("stdout", text);
697
736
  for (const text of message.stderr ?? []) stdio.push("stderr", text);
698
737
  }
738
+ var WORKER_UNAVAILABLE_CODE = "ERR_SCRIPT_WORKER_UNAVAILABLE";
739
+ function workerUnavailableError(error) {
740
+ const message = error instanceof Error && error.message ? error.message : "JavaScript worker sandbox is unavailable.";
741
+ return Object.assign(new Error(message), { code: WORKER_UNAVAILABLE_CODE });
742
+ }
743
+ function isSandboxWorkerUnavailable(error) {
744
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === WORKER_UNAVAILABLE_CODE);
745
+ }
746
+ function canUseSandboxWorker() {
747
+ return typeof Worker !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function" && typeof URL.revokeObjectURL === "function";
748
+ }
749
+ async function executeInSandboxWorker(code, timeoutMs, stdio, signal) {
750
+ if (!canUseSandboxWorker()) throw workerUnavailableError(/* @__PURE__ */ new Error("JavaScript worker sandbox needs Worker, Blob, and object URLs."));
751
+ if (signal?.aborted) throw abortError();
752
+ const messageId = `ox-code-play-${Math.random().toString(36).slice(2)}`;
753
+ const url = URL.createObjectURL(new Blob([buildJavaScriptWorkerSource()], { type: "text/javascript" }));
754
+ let worker;
755
+ try {
756
+ worker = new Worker(url);
757
+ } catch (error) {
758
+ URL.revokeObjectURL(url);
759
+ throw workerUnavailableError(error);
760
+ }
761
+ return new Promise((resolve, reject) => {
762
+ let settled = false;
763
+ const cleanup = () => {
764
+ if (settled) return;
765
+ settled = true;
766
+ clearTimeout(timer);
767
+ signal?.removeEventListener("abort", onAbort);
768
+ worker.onmessage = null;
769
+ worker.onerror = null;
770
+ worker.terminate();
771
+ URL.revokeObjectURL(url);
772
+ };
773
+ const onAbort = () => {
774
+ cleanup();
775
+ reject(abortError());
776
+ };
777
+ const onMessage = (event) => {
778
+ if (event.data?.id !== messageId) return;
779
+ cleanup();
780
+ applySandboxStreams(stdio, event.data);
781
+ if (event.data.error) {
782
+ reject(new Error(event.data.error));
783
+ return;
784
+ }
785
+ resolve(event.data.value);
786
+ };
787
+ const onError = (event) => {
788
+ cleanup();
789
+ reject(workerUnavailableError(new Error(event.message || "JavaScript worker sandbox failed.")));
790
+ };
791
+ const timer = setTimeout(() => {
792
+ cleanup();
793
+ reject(Object.assign(/* @__PURE__ */ new Error("JavaScript execution timed out."), { code: "ERR_SCRIPT_EXECUTION_TIMEOUT" }));
794
+ }, timeoutMs);
795
+ worker.onmessage = onMessage;
796
+ worker.onerror = onError;
797
+ signal?.addEventListener("abort", onAbort, { once: true });
798
+ worker.postMessage({
799
+ id: messageId,
800
+ code
801
+ });
802
+ });
803
+ }
699
804
  async function executeInSandboxIframe(code, timeoutMs, stdio, signal) {
700
805
  if (typeof document === "undefined" || typeof window === "undefined") throw new Error("JavaScript sandbox iframe needs a document.");
701
806
  if (signal?.aborted) throw abortError();
@@ -747,24 +852,23 @@ async function runJavaScript(request) {
747
852
  const tracker = new PhaseTracker();
748
853
  tracker.start("execute", "Execute");
749
854
  const stdio = new StdioBuffer(tracker.startedAt);
750
- const provenance = { execute: {
751
- host: "local",
752
- runtime: hasNodeVm() ? "node:vm" : "iframe",
753
- sandbox: hasNodeVm() ? "vm" : "srcdoc"
754
- } };
855
+ const runtime = currentJavaScriptRuntime();
856
+ let executedRuntime = runtime;
755
857
  try {
756
- const value = await executeScript(request.code, request.timeoutMs, stdio, request.signal);
858
+ const result = await executeScriptWithRuntime(request.code, request.timeoutMs, stdio, request.signal, runtime);
859
+ executedRuntime = result.runtime;
757
860
  tracker.stop();
758
861
  return {
759
862
  status: "ok",
760
863
  stdio: stdio.snapshot(),
761
864
  diagnostics: [],
762
- provenance,
865
+ provenance: { execute: javascriptRuntimeProvenance(executedRuntime) },
763
866
  timing: tracker.report(),
764
- value: value === void 0 ? void 0 : String(value)
867
+ value: result.value === void 0 ? void 0 : String(result.value)
765
868
  };
766
869
  } catch (error) {
767
870
  if (isAbortError(error) || request.signal?.aborted) throw error;
871
+ executedRuntime = executionRuntimeFromError(error) ?? executedRuntime;
768
872
  tracker.stop();
769
873
  const diagnostic = toDiagnostic(error);
770
874
  stdio.push("stderr", `${diagnostic.message}\n`);
@@ -772,12 +876,30 @@ async function runJavaScript(request) {
772
876
  status: isTimeout(error) ? "timeout" : "error",
773
877
  stdio: stdio.snapshot(),
774
878
  diagnostics: [diagnostic],
775
- provenance,
879
+ provenance: { execute: javascriptRuntimeProvenance(executedRuntime) },
776
880
  timing: tracker.report()
777
881
  };
778
882
  }
779
883
  }
780
- async function executeScript(code, timeoutMs, stdio, signal) {
884
+ async function executeScriptWithRuntime(code, timeoutMs, stdio, signal, runtime = currentJavaScriptRuntime()) {
885
+ try {
886
+ return {
887
+ value: await executeScriptInRuntime(code, timeoutMs, stdio, signal, runtime),
888
+ runtime
889
+ };
890
+ } catch (error) {
891
+ if (runtime === "worker" && isSandboxWorkerUnavailable(error) && typeof document !== "undefined") try {
892
+ return {
893
+ value: await executeScriptInRuntime(code, timeoutMs, stdio, signal, "iframe"),
894
+ runtime: "iframe"
895
+ };
896
+ } catch (fallbackError) {
897
+ throw withExecutionRuntime(fallbackError, "iframe");
898
+ }
899
+ throw withExecutionRuntime(error, runtime);
900
+ }
901
+ }
902
+ async function executeScriptInRuntime(code, timeoutMs, stdio, signal, runtime) {
781
903
  const consoleLike = {
782
904
  log: (...args) => stdio.push("stdout", formatConsoleArgs(args)),
783
905
  info: (...args) => stdio.push("stdout", formatConsoleArgs(args)),
@@ -785,7 +907,7 @@ async function executeScript(code, timeoutMs, stdio, signal) {
785
907
  error: (...args) => stdio.push("stderr", formatConsoleArgs(args))
786
908
  };
787
909
  if (signal?.aborted) throw abortError();
788
- if (javascriptExecuteRuntime(hasNodeVm(), typeof document !== "undefined") === "vm") {
910
+ if (runtime === "vm") {
789
911
  const vm = await import("node:vm");
790
912
  const context = vm.createContext({ console: consoleLike });
791
913
  return vm.runInContext(code, context, {
@@ -793,12 +915,45 @@ async function executeScript(code, timeoutMs, stdio, signal) {
793
915
  displayErrors: true
794
916
  });
795
917
  }
918
+ if (runtime === "worker") return executeInSandboxWorker(code, timeoutMs, stdio, signal);
796
919
  return executeInSandboxIframe(code, timeoutMs, stdio, signal);
797
920
  }
798
- function javascriptExecuteRuntime(hasVm, hasDocument) {
921
+ function currentJavaScriptRuntime() {
922
+ return javascriptExecuteRuntime(hasNodeVm(), canUseSandboxWorker(), typeof document !== "undefined");
923
+ }
924
+ function javascriptExecuteRuntime(hasVm, hasWorker, hasDocument) {
799
925
  if (hasVm) return "vm";
926
+ if (hasWorker) return "worker";
800
927
  if (hasDocument) return "iframe";
801
- throw new Error("JavaScript execute needs node:vm or a document for the sandbox iframe.");
928
+ throw new Error("JavaScript execute needs node:vm, a browser worker sandbox, or a document for the sandbox iframe.");
929
+ }
930
+ function javascriptRuntimeProvenance(runtime) {
931
+ if (runtime === "vm") return {
932
+ host: "local",
933
+ runtime: "node:vm",
934
+ sandbox: "vm"
935
+ };
936
+ if (runtime === "worker") return {
937
+ host: "local",
938
+ runtime: "web-worker",
939
+ sandbox: "worker"
940
+ };
941
+ return {
942
+ host: "local",
943
+ runtime: "iframe",
944
+ sandbox: "srcdoc"
945
+ };
946
+ }
947
+ function withExecutionRuntime(error, runtime) {
948
+ if (error instanceof Error) return Object.assign(error, { executionRuntime: runtime });
949
+ if (isErrorLike(error)) return Object.assign(error, { executionRuntime: runtime });
950
+ return Object.assign(new Error(String(error)), { executionRuntime: runtime });
951
+ }
952
+ function executionRuntimeFromError(error) {
953
+ if (error && typeof error === "object" && "executionRuntime" in error && isJavaScriptExecutionRuntime(error.executionRuntime)) return error.executionRuntime;
954
+ }
955
+ function isJavaScriptExecutionRuntime(value) {
956
+ return value === "vm" || value === "worker" || value === "iframe";
802
957
  }
803
958
  function isTimeout(error) {
804
959
  return Boolean(error && typeof error === "object" && "code" in error && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT");
@@ -1065,9 +1220,12 @@ async function runTypeScript(request) {
1065
1220
  const stdio = new StdioBuffer(tracker.startedAt);
1066
1221
  tracker.start("compile", "Strip types");
1067
1222
  const javascript = stripTypeScript(request.code);
1223
+ const runtime = currentJavaScriptRuntime();
1224
+ let executedRuntime = runtime;
1068
1225
  tracker.start("execute", "Execute");
1069
1226
  try {
1070
- const value = await executeScript(javascript, request.timeoutMs, stdio, request.signal);
1227
+ const result = await executeScriptWithRuntime(javascript, request.timeoutMs, stdio, request.signal, runtime);
1228
+ executedRuntime = result.runtime;
1071
1229
  tracker.stop();
1072
1230
  return {
1073
1231
  status: "ok",
@@ -1078,14 +1236,10 @@ async function runTypeScript(request) {
1078
1236
  host: "local",
1079
1237
  runtime: "strip-types"
1080
1238
  },
1081
- execute: {
1082
- host: "local",
1083
- runtime: hasNodeVm() ? "node:vm" : "iframe",
1084
- sandbox: hasNodeVm() ? "vm" : "srcdoc"
1085
- }
1239
+ execute: javascriptRuntimeProvenance(executedRuntime)
1086
1240
  },
1087
1241
  timing: tracker.report(),
1088
- value: value === void 0 ? void 0 : String(value)
1242
+ value: result.value === void 0 ? void 0 : String(result.value)
1089
1243
  };
1090
1244
  } catch (error) {
1091
1245
  if (isAbortError(error) || request.signal?.aborted) throw error;
@@ -1105,11 +1259,7 @@ async function runTypeScript(request) {
1105
1259
  host: "local",
1106
1260
  runtime: "strip-types"
1107
1261
  },
1108
- execute: {
1109
- host: "local",
1110
- runtime: hasNodeVm() ? "node:vm" : "iframe",
1111
- sandbox: hasNodeVm() ? "vm" : "srcdoc"
1112
- }
1262
+ execute: javascriptRuntimeProvenance(executedRuntime)
1113
1263
  },
1114
1264
  timing: tracker.report()
1115
1265
  };
@@ -1282,9 +1432,12 @@ function errorMessage(error) {
1282
1432
  }
1283
1433
  function friendlyTransportMessage(error) {
1284
1434
  const message = errorMessage(error);
1285
- if ((error instanceof TypeError || error instanceof Error && error.name === "TypeError") && /failed to fetch|networkerror|load failed|network request failed/i.test(message)) return "The executor could not be reached from this page (often CORS). Set endpoints to a host that allows browser POST, or use the Vite dev proxy.";
1435
+ if (isOfflineError(error)) return "The executor is offline or unreachable from this page (for example, CORS). Set endpoints to a host that allows browser POST, or use the Vite dev proxy.";
1286
1436
  return message;
1287
1437
  }
1438
+ function transportFailureStatus(error) {
1439
+ return isOfflineError(error) ? "offline" : "error";
1440
+ }
1288
1441
  function errorResult(message, source = "code-play", status = "error") {
1289
1442
  return withStdioText({
1290
1443
  status,
@@ -1298,6 +1451,14 @@ function errorResult(message, source = "code-play", status = "error") {
1298
1451
  timing: emptyTiming()
1299
1452
  });
1300
1453
  }
1454
+ function isOfflineError(error) {
1455
+ const message = errorMessage(error);
1456
+ const name = error && typeof error === "object" && "name" in error ? String(error.name) : void 0;
1457
+ const typeError = error instanceof TypeError || name === "TypeError";
1458
+ if (name === "MissingTransportError") return true;
1459
+ if (typeError && /failed to fetch|networkerror|load failed|network request failed/i.test(message)) return true;
1460
+ return /\boffline\b|no code play transport|network request failed/i.test(message);
1461
+ }
1301
1462
  //#endregion
1302
1463
  //#region src/session.ts
1303
1464
  var CodePlaySession = class {
@@ -1375,7 +1536,7 @@ var CodePlaySession = class {
1375
1536
  return this.finish(result);
1376
1537
  } catch (error) {
1377
1538
  if (signal.aborted || isAbortError(error)) return this.finish(errorResult("Run cancelled.", "code-play", "cancelled"));
1378
- return this.finish(errorResult(friendlyTransportMessage(error)));
1539
+ return this.finish(errorResult(friendlyTransportMessage(error), "code-play", transportFailureStatus(error)));
1379
1540
  }
1380
1541
  }
1381
1542
  finish(result) {
@@ -1460,12 +1621,39 @@ async function runPlayAction(input) {
1460
1621
  input.setBusy(false);
1461
1622
  }
1462
1623
  }
1624
+ function idleRunActionState() {
1625
+ return { phase: "idle" };
1626
+ }
1627
+ function runningRunActionState(action, startedAtMs = Date.now()) {
1628
+ return {
1629
+ phase: "running",
1630
+ action,
1631
+ startedAtMs
1632
+ };
1633
+ }
1634
+ function resultRunActionState(action, result, finishedAtMs = Date.now()) {
1635
+ return {
1636
+ phase: result.status === "offline" ? "offline" : result.status === "ok" || result.status === "cancelled" ? "result" : "error",
1637
+ action,
1638
+ result,
1639
+ message: result.diagnostics[0]?.message,
1640
+ finishedAtMs
1641
+ };
1642
+ }
1643
+ function errorRunActionState(action, error, finishedAtMs = Date.now()) {
1644
+ return {
1645
+ phase: "error",
1646
+ action,
1647
+ message: errorMessage(error),
1648
+ finishedAtMs
1649
+ };
1650
+ }
1463
1651
  //#endregion
1464
1652
  //#region src/styles.ts
1465
1653
  var CODE_PLAY_STYLES = `
1466
1654
  .ox-code-play {
1467
1655
  border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 16%, transparent));
1468
- border-radius: 12px;
1656
+ border-radius: 8px;
1469
1657
  background: var(--octc-color-bg-alt, var(--octc-color-bg, Canvas));
1470
1658
  color: var(--octc-color-text, CanvasText);
1471
1659
  overflow: hidden;
@@ -1479,22 +1667,89 @@ var CODE_PLAY_STYLES = `
1479
1667
  padding: 0.6rem 0.8rem;
1480
1668
  border-bottom: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 12%, transparent));
1481
1669
  }
1670
+ .ox-code-play__summary {
1671
+ display: inline-flex;
1672
+ flex: 1 1 12rem;
1673
+ min-width: 9rem;
1674
+ gap: 0.45rem;
1675
+ align-items: baseline;
1676
+ }
1482
1677
  .ox-code-play__lang {
1483
1678
  font: 600 0.8rem/1.2 ui-sans-serif, system-ui, sans-serif;
1484
- margin-right: auto;
1485
1679
  color: var(--octc-color-text, CanvasText);
1486
1680
  }
1681
+ .ox-code-play__title {
1682
+ min-width: 0;
1683
+ overflow-wrap: anywhere;
1684
+ font: 500 0.78rem/1.25 ui-sans-serif, system-ui, sans-serif;
1685
+ opacity: 0.72;
1686
+ }
1687
+ .ox-code-play__status {
1688
+ min-width: 5.25rem;
1689
+ text-align: center;
1690
+ border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 16%, transparent));
1691
+ border-radius: 6px;
1692
+ padding: 0.18rem 0.5rem;
1693
+ font: 650 0.68rem/1.25 ui-sans-serif, system-ui, sans-serif;
1694
+ color: var(--octc-color-text, CanvasText);
1695
+ background: var(--octc-color-bg, Canvas);
1696
+ }
1697
+ .ox-code-play[data-ox-run-state="running"] .ox-code-play__status {
1698
+ color: var(--octc-color-primary, var(--octc-accent, #4f46e5));
1699
+ }
1700
+ .ox-code-play[data-ox-run-state="error"] .ox-code-play__status,
1701
+ .ox-code-play[data-ox-run-state="offline"] .ox-code-play__status {
1702
+ color: var(--octc-danger, #b42318);
1703
+ }
1487
1704
  .ox-code-play__toolbar button {
1488
1705
  appearance: none;
1489
1706
  border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 20%, transparent));
1490
1707
  background: color-mix(in srgb, var(--octc-color-text, CanvasText) 8%, transparent);
1491
1708
  color: var(--octc-color-text, CanvasText);
1492
- border-radius: 999px;
1709
+ border-radius: 8px;
1493
1710
  padding: 0.25rem 0.75rem;
1494
1711
  font: 600 0.75rem/1.4 ui-sans-serif, system-ui, sans-serif;
1495
1712
  cursor: pointer;
1496
1713
  }
1497
1714
  .ox-code-play__toolbar button:disabled { opacity: 0.55; cursor: progress; }
1715
+ .ox-code-play__toolbar button:focus-visible,
1716
+ .ox-code-play__tabs button:focus-visible,
1717
+ .ox-code-play__field input:focus-visible,
1718
+ .ox-code-play__field select:focus-visible,
1719
+ .ox-code-play__panel:focus-visible {
1720
+ outline: 2px solid var(--octc-color-primary, var(--octc-accent, #4f46e5));
1721
+ outline-offset: 2px;
1722
+ }
1723
+ .ox-code-play__runtime {
1724
+ display: flex;
1725
+ flex-wrap: wrap;
1726
+ gap: 0.45rem;
1727
+ align-items: center;
1728
+ padding: 0.55rem 0.8rem;
1729
+ border-bottom: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 10%, transparent));
1730
+ background: color-mix(in srgb, var(--octc-color-text, CanvasText) 4%, transparent);
1731
+ }
1732
+ .ox-code-play__runtime-chip {
1733
+ display: inline-grid;
1734
+ gap: 0.1rem;
1735
+ min-width: 7.5rem;
1736
+ padding: 0.35rem 0.5rem;
1737
+ border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 14%, transparent));
1738
+ border-radius: 6px;
1739
+ background: var(--octc-color-bg, Canvas);
1740
+ }
1741
+ .ox-code-play__runtime-chip span {
1742
+ font: 600 0.62rem/1.1 ui-sans-serif, system-ui, sans-serif;
1743
+ text-transform: uppercase;
1744
+ letter-spacing: 0.04em;
1745
+ opacity: 0.62;
1746
+ }
1747
+ .ox-code-play__runtime-chip strong {
1748
+ font: 650 0.78rem/1.2 ui-sans-serif, system-ui, sans-serif;
1749
+ }
1750
+ .ox-code-play__runtime-chip--ok strong { color: var(--octc-color-primary, var(--octc-accent, #4f46e5)); }
1751
+ .ox-code-play__runtime-chip--warn strong { color: var(--octc-warning, #b54708); }
1752
+ .ox-code-play__runtime-chip--muted strong { opacity: 0.72; }
1498
1753
  .ox-code-play .ox-code { margin: 0; }
1499
1754
  .ox-code-play__source pre { margin: 0; border: 0; border-radius: 0; }
1500
1755
  .ox-code-play__tabs {
@@ -1508,7 +1763,7 @@ var CODE_PLAY_STYLES = `
1508
1763
  background: transparent;
1509
1764
  color: var(--octc-color-text, CanvasText);
1510
1765
  padding: 0.35rem 0.55rem;
1511
- border-radius: 8px 8px 0 0;
1766
+ border-radius: 6px 6px 0 0;
1512
1767
  font: 600 0.75rem/1.2 ui-sans-serif, system-ui, sans-serif;
1513
1768
  cursor: pointer;
1514
1769
  }
@@ -1535,7 +1790,7 @@ var CODE_PLAY_STYLES = `
1535
1790
  .ox-code-play__field input, .ox-code-play__field select {
1536
1791
  font: inherit;
1537
1792
  padding: 0.3rem 0.45rem;
1538
- border-radius: 8px;
1793
+ border-radius: 6px;
1539
1794
  border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 18%, transparent));
1540
1795
  background: var(--octc-color-bg, Canvas);
1541
1796
  color: var(--octc-color-text, CanvasText);
@@ -1630,42 +1885,127 @@ function renderPlayUi(state) {
1630
1885
  if (preset === "headless") return "";
1631
1886
  const panel = state.panel ?? "stdio";
1632
1887
  const canTypecheck = state.payload.capabilities.typecheck;
1888
+ const runState = state.runState ?? (state.busy ? {
1889
+ phase: "running",
1890
+ action: "execute"
1891
+ } : idleRunActionState());
1892
+ const isBusy = Boolean(state.busy) || runState.phase === "running";
1633
1893
  const tabs = renderTabs(state, panel);
1634
1894
  const viewers = state.payload.viewers;
1635
- return `<div class="ox-code-play ox-code-play--${preset}" data-ox-code-play-ui>
1895
+ const label = widgetLabel(state.payload, definition);
1896
+ return `<div class="ox-code-play ox-code-play--${preset}" data-ox-code-play-ui data-ox-run-state="${runState.phase}" role="region" aria-label="${escapeHtml(label)}" aria-busy="${isBusy ? "true" : "false"}">
1636
1897
  <div class="ox-code-play__toolbar">
1637
- <span class="ox-code-play__lang">${escapeHtml(definition?.name ?? state.payload.language)}</span>
1638
- <button type="button" data-ox-action="run"${actionButtonAttrs("run", Boolean(state.busy))}>Run</button>
1639
- ${canTypecheck ? `<button type="button" data-ox-action="typecheck"${actionButtonAttrs("typecheck", Boolean(state.busy))}>Typecheck</button>` : ""}
1640
- <button type="button" data-ox-action="cancel"${actionButtonAttrs("cancel", Boolean(state.busy))}>Cancel</button>
1898
+ <span class="ox-code-play__summary"><span class="ox-code-play__lang">${escapeHtml(definition?.name ?? state.payload.language)}</span>${state.payload.title ? `<span class="ox-code-play__title">${escapeHtml(state.payload.title)}</span>` : ""}</span>
1899
+ <span class="ox-code-play__status" data-ox-status role="status" aria-live="polite">${renderRunStatusText(runState)}</span>
1900
+ <button type="button" data-ox-action="run" aria-label="${escapeHtml(`Run ${label}`)}"${actionButtonAttrs("run", Boolean(state.busy))}>Run</button>
1901
+ ${canTypecheck ? `<button type="button" data-ox-action="typecheck" aria-label="${escapeHtml(`Typecheck ${label}`)}"${actionButtonAttrs("typecheck", Boolean(state.busy))}>Typecheck</button>` : ""}
1902
+ <button type="button" data-ox-action="cancel" aria-label="${escapeHtml(`Cancel ${label}`)}"${actionButtonAttrs("cancel", Boolean(state.busy))}>Cancel</button>
1641
1903
  </div>
1904
+ ${renderRuntimeStrip(state.payload, definition)}
1642
1905
  <div class="ox-code-play__source"></div>
1643
1906
  ${tabs}
1644
- ${viewers.stdio ? `<div class="ox-code-play__panel" data-panel="stdio">${renderDiagnosticsHtml(state.result)}${renderStdioHtml(state.result?.stdio ?? [])}</div>` : ""}
1645
- ${viewers.stderr ? `<div class="ox-code-play__panel" data-panel="stderr"${hidden(panel, "stderr", preset)}>${renderStderrHtml(state.result)}</div>` : ""}
1646
- <div class="ox-code-play__panel" data-panel="config"${hidden(panel, "config", preset)}>${renderConfigHtml(definition?.configSchema ?? [], state.payload.config)}</div>
1647
- <div class="ox-code-play__panel" data-panel="provenance"${hidden(panel, "provenance", preset)}>${renderProvenanceHtml(state.result?.provenance)}</div>
1648
- <div class="ox-code-play__panel" data-panel="timing"${hidden(panel, "timing", preset)}>${renderTimingHtml(state.result?.timing)}</div>
1907
+ ${viewers.stdio ? renderPanel("stdio", "Stdio", panel, preset, `${renderDiagnosticsHtml(state.result)}${renderStdioHtml(state.result?.stdio ?? [])}`) : ""}
1908
+ ${viewers.stderr ? renderPanel("stderr", "Stderr", panel, preset, renderStderrHtml(state.result)) : ""}
1909
+ ${viewers.config ? renderPanel("config", "Config", panel, preset, renderConfigHtml(definition?.configSchema ?? [], state.payload.config)) : ""}
1910
+ ${viewers.provenance ? renderPanel("provenance", "Provenance", panel, preset, renderProvenanceHtml(state.result?.provenance)) : ""}
1911
+ ${viewers.timing ? renderPanel("timing", "Timing", panel, preset, renderTimingHtml(state.result?.timing)) : ""}
1649
1912
  </div>`;
1650
1913
  }
1914
+ function renderRunStatusText(state) {
1915
+ if (state.phase === "running") return state.action === "typecheck" ? "Typechecking" : "Running";
1916
+ if (state.phase === "offline") return "Offline";
1917
+ if (state.phase === "error") {
1918
+ if (state.result?.status === "timeout") return "Timed out";
1919
+ if (state.result?.status === "unsupported") return "Unsupported";
1920
+ return "Error";
1921
+ }
1922
+ if (state.phase === "result") {
1923
+ if (state.result?.status === "cancelled") return "Cancelled";
1924
+ return "Done";
1925
+ }
1926
+ return "Ready";
1927
+ }
1928
+ function renderRuntimeStrip(payload, definition) {
1929
+ const runtime = runtimeLabel(payload, definition);
1930
+ const executor = executorLabel(payload, definition);
1931
+ const checks = payload.capabilities.typecheck ? "Typecheck ready" : "Run only";
1932
+ return `<div class="ox-code-play__runtime" aria-label="Code Play runtime">${[
1933
+ runtimeChip("Runtime", runtime.label, runtime.kind),
1934
+ runtimeChip("Executor", executor.label, executor.kind),
1935
+ runtimeChip("Checks", checks, payload.capabilities.typecheck ? "ok" : "muted")
1936
+ ].join("")}</div>`;
1937
+ }
1938
+ function runtimeLabel(payload, definition) {
1939
+ switch (definition?.backend) {
1940
+ case "javascript": return {
1941
+ label: "Browser sandbox",
1942
+ kind: "ok"
1943
+ };
1944
+ case "typescript": return {
1945
+ label: "TypeScript sandbox",
1946
+ kind: "ok"
1947
+ };
1948
+ case "framework": return {
1949
+ label: `${definition.name} iframe preview`,
1950
+ kind: "ok"
1951
+ };
1952
+ case "rust-playground": return {
1953
+ label: "Rust Playground",
1954
+ kind: payload.endpoints?.rust ? "ok" : "warn"
1955
+ };
1956
+ case "go-playground": return {
1957
+ label: "Go Playground",
1958
+ kind: payload.endpoints?.go ? "ok" : "warn"
1959
+ };
1960
+ case "remote": return payload.endpoint ? {
1961
+ label: "Piston-compatible",
1962
+ kind: "ok"
1963
+ } : {
1964
+ label: "Endpoint missing",
1965
+ kind: "warn"
1966
+ };
1967
+ default: return {
1968
+ label: definition?.name ?? payload.language,
1969
+ kind: "muted"
1970
+ };
1971
+ }
1972
+ }
1973
+ function executorLabel(payload, definition) {
1974
+ if (!payload.capabilities.execute) return {
1975
+ label: "Disabled",
1976
+ kind: "warn"
1977
+ };
1978
+ if (definition?.backend === "remote" && !payload.endpoint) return {
1979
+ label: "Configure endpoint",
1980
+ kind: "warn"
1981
+ };
1982
+ return {
1983
+ label: "On demand",
1984
+ kind: "ok"
1985
+ };
1986
+ }
1987
+ function runtimeChip(label, value, kind) {
1988
+ return `<span class="ox-code-play__runtime-chip ox-code-play__runtime-chip--${kind}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></span>`;
1989
+ }
1651
1990
  function renderTabs(state, panel) {
1652
1991
  if (state.payload.ui === "compact") return "";
1653
1992
  const viewers = state.payload.viewers;
1654
1993
  const buttons = [
1655
- viewers.stdio ? tab("stdio", "stdio", panel) : "",
1656
- viewers.stderr ? tab("stderr", "stderr", panel) : "",
1657
- viewers.config ? tab("config", "config", panel) : "",
1658
- viewers.provenance ? tab("provenance", "provenance", panel) : "",
1659
- viewers.timing ? tab("timing", "timing", panel) : ""
1994
+ viewers.stdio ? tab("stdio", "Stdio", panel) : "",
1995
+ viewers.stderr ? tab("stderr", "Stderr", panel) : "",
1996
+ viewers.config ? tab("config", "Config", panel) : "",
1997
+ viewers.provenance ? tab("provenance", "Provenance", panel) : "",
1998
+ viewers.timing ? tab("timing", "Timing", panel) : ""
1660
1999
  ].filter(Boolean).join("");
1661
2000
  return buttons ? `<div class="ox-code-play__tabs" role="tablist">${buttons}</div>` : "";
1662
2001
  }
1663
2002
  function tab(id, label, selected) {
1664
- return `<button type="button" role="tab" data-ox-panel="${id}" aria-selected="${selected === id ? "true" : "false"}">${label}</button>`;
2003
+ const isSelected = selected === id;
2004
+ return `<button type="button" role="tab" data-ox-panel="${id}" aria-selected="${isSelected ? "true" : "false"}" tabindex="${isSelected ? "0" : "-1"}">${label}</button>`;
1665
2005
  }
1666
2006
  function actionButtonAttrs(action, busy) {
1667
2007
  const state = actionBusyState(action, busy);
1668
- return `${state.disabled ? " disabled" : ""}${state.hidden ? " hidden" : ""}`;
2008
+ return `${state.disabled ? " disabled aria-disabled=\"true\"" : ""}${state.hidden ? " hidden" : ""}`;
1669
2009
  }
1670
2010
  function actionBusyState(action, busy) {
1671
2011
  if (action === "cancel") return {
@@ -1682,15 +2022,30 @@ function applyActionBusy(root, busy) {
1682
2022
  const state = actionBusyState(button.dataset.oxAction ?? "", busy);
1683
2023
  button.disabled = state.disabled;
1684
2024
  button.hidden = state.hidden;
2025
+ button.setAttribute("aria-disabled", state.disabled ? "true" : "false");
1685
2026
  }
2027
+ for (const widget of queryPlayWidgets(root)) widget.setAttribute("aria-busy", busy ? "true" : "false");
2028
+ }
2029
+ function renderPanel(id, label, current, preset, html) {
2030
+ return `<div class="ox-code-play__panel" role="tabpanel" tabindex="0" aria-label="${escapeHtml(label)}" data-panel="${id}"${hidden(current, id, preset)}>${html}</div>`;
1686
2031
  }
1687
2032
  function hidden(current, id, preset) {
1688
2033
  if (preset === "compact" && (id === "stdio" || id === "stderr")) return "";
1689
2034
  return current === id ? "" : " hidden";
1690
2035
  }
2036
+ function widgetLabel(payload, definition) {
2037
+ const language = definition?.name ?? payload.language;
2038
+ return payload.title ? `${payload.title} (${language})` : `${language} Code Play`;
2039
+ }
2040
+ function queryPlayWidgets(root) {
2041
+ const widgets = [...root.querySelectorAll(".ox-code-play")];
2042
+ if (typeof HTMLElement !== "undefined" && root instanceof HTMLElement && root.matches(".ox-code-play")) widgets.unshift(root);
2043
+ return widgets;
2044
+ }
1691
2045
  //#endregion
1692
2046
  //#region src/hydrate.ts
1693
2047
  var STYLE_ID = "ox-code-play-styles";
2048
+ var widgetId = 0;
1694
2049
  function hydrateCodePlay(root = defaultRoot(), options = {}) {
1695
2050
  ensureStyles();
1696
2051
  const client = options.client ?? createCodePlayFromPayloads(root);
@@ -1703,7 +2058,7 @@ function mountCodePlay(element, options = {}) {
1703
2058
  const payload = readPlayPayload(element.getAttribute("data-ox-code-play") ?? "");
1704
2059
  if (!payload || payload.ui === "headless") return;
1705
2060
  const client = options.client ?? createCodePlay({
1706
- languages: { [payload.language]: true },
2061
+ languages: { [payload.language]: languageEnableFromPayload(payload) },
1707
2062
  endpoints: payload.endpoints
1708
2063
  });
1709
2064
  const source = element.innerHTML;
@@ -1715,7 +2070,9 @@ function mountCodePlay(element, options = {}) {
1715
2070
  if (sourceSlot) sourceSlot.innerHTML = source;
1716
2071
  element.replaceChildren(widget);
1717
2072
  element.dataset.oxCodePlayMounted = "true";
2073
+ element.removeAttribute("inert");
1718
2074
  bindWidget(element, payload, client);
2075
+ prepareA11y(element);
1719
2076
  }
1720
2077
  function bindWidget(element, payload, client) {
1721
2078
  let current = payload;
@@ -1730,6 +2087,9 @@ function bindWidget(element, payload, client) {
1730
2087
  runButton?.addEventListener("click", () => void run("execute"));
1731
2088
  checkButton?.addEventListener("click", () => void run("typecheck"));
1732
2089
  cancelButton?.addEventListener("click", () => session.cancel());
2090
+ element.addEventListener("keydown", (event) => {
2091
+ if (event.target instanceof HTMLElement && event.target.matches("[role=\"tab\"]")) handleTabKeydown(element, event);
2092
+ });
1733
2093
  element.addEventListener("click", (event) => {
1734
2094
  const target = event.target;
1735
2095
  if (!(target instanceof HTMLElement)) return;
@@ -1748,13 +2108,20 @@ function bindWidget(element, payload, client) {
1748
2108
  async function run(action) {
1749
2109
  await runPlayAction({
1750
2110
  action: () => action === "typecheck" ? session.typecheck() : session.run(),
1751
- setBusy: (busy) => applyActionBusy(element, busy),
1752
- onResult: (result) => paintResult(element, current, result),
1753
- onError: (error) => paintResult(element, current, errorResult(errorMessage(error)))
2111
+ setBusy: (busy) => {
2112
+ applyActionBusy(element, busy);
2113
+ if (busy) paintRunState(element, runningRunActionState(action));
2114
+ },
2115
+ onResult: (result) => paintResult(element, current, result, action),
2116
+ onError: (error) => {
2117
+ paintRunState(element, errorRunActionState(action, error));
2118
+ paintResult(element, current, errorResult(errorMessage(error)), action);
2119
+ }
1754
2120
  });
1755
2121
  }
1756
2122
  }
1757
- function paintResult(element, _payload, result) {
2123
+ function paintResult(element, _payload, result, action) {
2124
+ paintRunState(element, resultRunActionState(action, result));
1758
2125
  const stdio = element.querySelector("[data-panel=\"stdio\"]");
1759
2126
  if (stdio) {
1760
2127
  stdio.innerHTML = `${renderDiagnosticsHtml(result)}${renderStdioHtml(result.stdio)}`;
@@ -1775,11 +2142,27 @@ function paintResult(element, _payload, result) {
1775
2142
  if (timing) timing.innerHTML = renderTimingHtml(result.timing);
1776
2143
  const stderr = element.querySelector("[data-panel=\"stderr\"]");
1777
2144
  if (stderr) stderr.innerHTML = renderStderrHtml(result);
1778
- showPanel(element, Boolean(stderr) && (Boolean(result.stderr) || result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) ? "stderr" : "stdio");
2145
+ showPanel(element, resultPanelToShow(result, Boolean(stderr)));
2146
+ }
2147
+ function paintRunState(element, state) {
2148
+ const widget = element.querySelector(".ox-code-play");
2149
+ widget?.setAttribute("data-ox-run-state", state.phase);
2150
+ widget?.setAttribute("aria-busy", state.phase === "running" ? "true" : "false");
2151
+ const status = element.querySelector("[data-ox-status]");
2152
+ if (status) status.textContent = renderRunStatusText(state);
2153
+ }
2154
+ function resultPanelToShow(result, hasStderrPanel) {
2155
+ if (!hasStderrPanel) return "stdio";
2156
+ if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error") || result.status !== "ok" && Boolean(result.stderr)) return "stderr";
2157
+ return "stdio";
1779
2158
  }
1780
2159
  function showPanel(element, panel) {
1781
2160
  const compact = Boolean(element.querySelector(".ox-code-play--compact"));
1782
- for (const tab of element.querySelectorAll("[data-ox-panel]")) tab.setAttribute("aria-selected", tab.getAttribute("data-ox-panel") === panel ? "true" : "false");
2161
+ for (const tab of element.querySelectorAll("[data-ox-panel]")) {
2162
+ const selected = tab.getAttribute("data-ox-panel") === panel;
2163
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
2164
+ tab.tabIndex = selected ? 0 : -1;
2165
+ }
1783
2166
  for (const node of element.querySelectorAll(".ox-code-play__panel")) {
1784
2167
  const id = node.dataset.panel;
1785
2168
  if (compact && (id === "stdio" || id === "stderr")) {
@@ -1789,6 +2172,38 @@ function showPanel(element, panel) {
1789
2172
  node.hidden = id !== panel;
1790
2173
  }
1791
2174
  }
2175
+ function handleTabKeydown(element, event) {
2176
+ if (![
2177
+ "ArrowLeft",
2178
+ "ArrowRight",
2179
+ "Home",
2180
+ "End"
2181
+ ].includes(event.key)) return;
2182
+ const tabs = [...element.querySelectorAll("[data-ox-panel]")];
2183
+ const current = tabs.indexOf(event.target);
2184
+ if (current === -1) return;
2185
+ event.preventDefault();
2186
+ const next = tabs[event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : event.key === "ArrowRight" ? (current + 1) % tabs.length : (current - 1 + tabs.length) % tabs.length];
2187
+ const panel = next?.dataset.oxPanel;
2188
+ if (!next || !panel) return;
2189
+ showPanel(element, panel);
2190
+ next.focus();
2191
+ }
2192
+ function prepareA11y(element) {
2193
+ const widget = element.querySelector(".ox-code-play");
2194
+ if (!widget) return;
2195
+ if (!widget.id) widget.id = `ox-code-play-${++widgetId}`;
2196
+ for (const tab of element.querySelectorAll("[data-ox-panel]")) {
2197
+ const panelName = tab.dataset.oxPanel;
2198
+ const panel = panelName ? element.querySelector(`[data-panel="${panelName}"]`) : null;
2199
+ if (!panelName || !panel) continue;
2200
+ tab.id ||= `${widget.id}-${panelName}-tab`;
2201
+ panel.id ||= `${widget.id}-${panelName}-panel`;
2202
+ tab.setAttribute("aria-controls", panel.id);
2203
+ panel.setAttribute("aria-labelledby", tab.id);
2204
+ panel.removeAttribute("aria-label");
2205
+ }
2206
+ }
1792
2207
  function readForm(form) {
1793
2208
  const data = new FormData(form);
1794
2209
  const config = {};
@@ -1801,7 +2216,7 @@ function createCodePlayFromPayloads(root) {
1801
2216
  let endpoints;
1802
2217
  for (const element of queryWidgets(root)) try {
1803
2218
  const payload = decodePayload(element.getAttribute("data-ox-code-play") ?? "");
1804
- languages[payload.language] = true;
2219
+ languages[payload.language] = languageEnableFromPayload(payload);
1805
2220
  endpoints = payload.endpoints ?? endpoints;
1806
2221
  } catch {}
1807
2222
  return createCodePlay({
@@ -1809,6 +2224,15 @@ function createCodePlayFromPayloads(root) {
1809
2224
  endpoints
1810
2225
  });
1811
2226
  }
2227
+ function languageEnableFromPayload(payload) {
2228
+ const enable = {
2229
+ execute: payload.capabilities.execute,
2230
+ typecheck: payload.capabilities.typecheck,
2231
+ config: payload.config
2232
+ };
2233
+ if (payload.endpoint) enable.endpoint = payload.endpoint;
2234
+ return enable;
2235
+ }
1812
2236
  function queryWidgets(root) {
1813
2237
  return [...root.querySelectorAll("[data-ox-code-play]")];
1814
2238
  }