@ox-content/code-play 3.0.0-alpha.8 → 3.0.0-beta.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/dist/browser.mjs CHANGED
@@ -372,6 +372,9 @@ function escapeHtml(value) {
372
372
  }
373
373
  });
374
374
  }
375
+ function escapeAttribute(value) {
376
+ return escapeHtml(value).replace(/`/g, "`");
377
+ }
375
378
  //#endregion
376
379
  //#region src/timing.ts
377
380
  var PhaseTracker = class {
@@ -692,10 +695,115 @@ function buildJavaScriptSandboxDocument(code, messageId) {
692
695
  })();
693
696
  <\/script></body></html>`;
694
697
  }
698
+ function buildJavaScriptWorkerSource() {
699
+ return `
700
+ (function () {
701
+ function format(args) {
702
+ return Array.prototype.map.call(args, function (value) {
703
+ if (typeof value === "string") return value;
704
+ if (value === undefined) return "undefined";
705
+ if (value === null) return "null";
706
+ try { return JSON.stringify(value); } catch (error) { return String(value); }
707
+ }).join(" ") + "\\n";
708
+ }
709
+ self.onmessage = function (event) {
710
+ var data = event.data || {};
711
+ if (!data.id) return;
712
+ var stdout = [];
713
+ var stderr = [];
714
+ var consoleLike = {
715
+ log: function () { stdout.push(format(arguments)); },
716
+ info: function () { stdout.push(format(arguments)); },
717
+ warn: function () { stderr.push(format(arguments)); },
718
+ error: function () { stderr.push(format(arguments)); }
719
+ };
720
+ try {
721
+ var run = new Function("console", '"use strict";\\n' + String(data.code || ""));
722
+ var value = run(consoleLike);
723
+ self.postMessage({
724
+ id: data.id,
725
+ stdout: stdout,
726
+ stderr: stderr,
727
+ value: value === undefined ? undefined : String(value)
728
+ });
729
+ } catch (error) {
730
+ var message = error && error.message ? String(error.message) : String(error);
731
+ self.postMessage({ id: data.id, stdout: stdout, stderr: stderr, error: message });
732
+ }
733
+ };
734
+ })();
735
+ `;
736
+ }
695
737
  function applySandboxStreams(stdio, message) {
696
738
  for (const text of message.stdout ?? []) stdio.push("stdout", text);
697
739
  for (const text of message.stderr ?? []) stdio.push("stderr", text);
698
740
  }
741
+ var WORKER_UNAVAILABLE_CODE = "ERR_SCRIPT_WORKER_UNAVAILABLE";
742
+ function workerUnavailableError(error) {
743
+ const message = error instanceof Error && error.message ? error.message : "JavaScript worker sandbox is unavailable.";
744
+ return Object.assign(new Error(message), { code: WORKER_UNAVAILABLE_CODE });
745
+ }
746
+ function isSandboxWorkerUnavailable(error) {
747
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === WORKER_UNAVAILABLE_CODE);
748
+ }
749
+ function canUseSandboxWorker() {
750
+ return typeof Worker !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function" && typeof URL.revokeObjectURL === "function";
751
+ }
752
+ async function executeInSandboxWorker(code, timeoutMs, stdio, signal) {
753
+ if (!canUseSandboxWorker()) throw workerUnavailableError(/* @__PURE__ */ new Error("JavaScript worker sandbox needs Worker, Blob, and object URLs."));
754
+ if (signal?.aborted) throw abortError();
755
+ const messageId = `ox-code-play-${Math.random().toString(36).slice(2)}`;
756
+ const url = URL.createObjectURL(new Blob([buildJavaScriptWorkerSource()], { type: "text/javascript" }));
757
+ let worker;
758
+ try {
759
+ worker = new Worker(url);
760
+ } catch (error) {
761
+ URL.revokeObjectURL(url);
762
+ throw workerUnavailableError(error);
763
+ }
764
+ return new Promise((resolve, reject) => {
765
+ let settled = false;
766
+ const cleanup = () => {
767
+ if (settled) return;
768
+ settled = true;
769
+ clearTimeout(timer);
770
+ signal?.removeEventListener("abort", onAbort);
771
+ worker.onmessage = null;
772
+ worker.onerror = null;
773
+ worker.terminate();
774
+ URL.revokeObjectURL(url);
775
+ };
776
+ const onAbort = () => {
777
+ cleanup();
778
+ reject(abortError());
779
+ };
780
+ const onMessage = (event) => {
781
+ if (event.data?.id !== messageId) return;
782
+ cleanup();
783
+ applySandboxStreams(stdio, event.data);
784
+ if (event.data.error) {
785
+ reject(new Error(event.data.error));
786
+ return;
787
+ }
788
+ resolve(event.data.value);
789
+ };
790
+ const onError = (event) => {
791
+ cleanup();
792
+ reject(workerUnavailableError(new Error(event.message || "JavaScript worker sandbox failed.")));
793
+ };
794
+ const timer = setTimeout(() => {
795
+ cleanup();
796
+ reject(Object.assign(/* @__PURE__ */ new Error("JavaScript execution timed out."), { code: "ERR_SCRIPT_EXECUTION_TIMEOUT" }));
797
+ }, timeoutMs);
798
+ worker.onmessage = onMessage;
799
+ worker.onerror = onError;
800
+ signal?.addEventListener("abort", onAbort, { once: true });
801
+ worker.postMessage({
802
+ id: messageId,
803
+ code
804
+ });
805
+ });
806
+ }
699
807
  async function executeInSandboxIframe(code, timeoutMs, stdio, signal) {
700
808
  if (typeof document === "undefined" || typeof window === "undefined") throw new Error("JavaScript sandbox iframe needs a document.");
701
809
  if (signal?.aborted) throw abortError();
@@ -747,24 +855,23 @@ async function runJavaScript(request) {
747
855
  const tracker = new PhaseTracker();
748
856
  tracker.start("execute", "Execute");
749
857
  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
- } };
858
+ const runtime = currentJavaScriptRuntime();
859
+ let executedRuntime = runtime;
755
860
  try {
756
- const value = await executeScript(request.code, request.timeoutMs, stdio, request.signal);
861
+ const result = await executeScriptWithRuntime(request.code, request.timeoutMs, stdio, request.signal, runtime);
862
+ executedRuntime = result.runtime;
757
863
  tracker.stop();
758
864
  return {
759
865
  status: "ok",
760
866
  stdio: stdio.snapshot(),
761
867
  diagnostics: [],
762
- provenance,
868
+ provenance: { execute: javascriptRuntimeProvenance(executedRuntime) },
763
869
  timing: tracker.report(),
764
- value: value === void 0 ? void 0 : String(value)
870
+ value: result.value === void 0 ? void 0 : String(result.value)
765
871
  };
766
872
  } catch (error) {
767
873
  if (isAbortError(error) || request.signal?.aborted) throw error;
874
+ executedRuntime = executionRuntimeFromError(error) ?? executedRuntime;
768
875
  tracker.stop();
769
876
  const diagnostic = toDiagnostic(error);
770
877
  stdio.push("stderr", `${diagnostic.message}\n`);
@@ -772,12 +879,30 @@ async function runJavaScript(request) {
772
879
  status: isTimeout(error) ? "timeout" : "error",
773
880
  stdio: stdio.snapshot(),
774
881
  diagnostics: [diagnostic],
775
- provenance,
882
+ provenance: { execute: javascriptRuntimeProvenance(executedRuntime) },
776
883
  timing: tracker.report()
777
884
  };
778
885
  }
779
886
  }
780
- async function executeScript(code, timeoutMs, stdio, signal) {
887
+ async function executeScriptWithRuntime(code, timeoutMs, stdio, signal, runtime = currentJavaScriptRuntime()) {
888
+ try {
889
+ return {
890
+ value: await executeScriptInRuntime(code, timeoutMs, stdio, signal, runtime),
891
+ runtime
892
+ };
893
+ } catch (error) {
894
+ if (runtime === "worker" && isSandboxWorkerUnavailable(error) && typeof document !== "undefined") try {
895
+ return {
896
+ value: await executeScriptInRuntime(code, timeoutMs, stdio, signal, "iframe"),
897
+ runtime: "iframe"
898
+ };
899
+ } catch (fallbackError) {
900
+ throw withExecutionRuntime(fallbackError, "iframe");
901
+ }
902
+ throw withExecutionRuntime(error, runtime);
903
+ }
904
+ }
905
+ async function executeScriptInRuntime(code, timeoutMs, stdio, signal, runtime) {
781
906
  const consoleLike = {
782
907
  log: (...args) => stdio.push("stdout", formatConsoleArgs(args)),
783
908
  info: (...args) => stdio.push("stdout", formatConsoleArgs(args)),
@@ -785,7 +910,7 @@ async function executeScript(code, timeoutMs, stdio, signal) {
785
910
  error: (...args) => stdio.push("stderr", formatConsoleArgs(args))
786
911
  };
787
912
  if (signal?.aborted) throw abortError();
788
- if (javascriptExecuteRuntime(hasNodeVm(), typeof document !== "undefined") === "vm") {
913
+ if (runtime === "vm") {
789
914
  const vm = await import("node:vm");
790
915
  const context = vm.createContext({ console: consoleLike });
791
916
  return vm.runInContext(code, context, {
@@ -793,12 +918,45 @@ async function executeScript(code, timeoutMs, stdio, signal) {
793
918
  displayErrors: true
794
919
  });
795
920
  }
921
+ if (runtime === "worker") return executeInSandboxWorker(code, timeoutMs, stdio, signal);
796
922
  return executeInSandboxIframe(code, timeoutMs, stdio, signal);
797
923
  }
798
- function javascriptExecuteRuntime(hasVm, hasDocument) {
924
+ function currentJavaScriptRuntime() {
925
+ return javascriptExecuteRuntime(hasNodeVm(), canUseSandboxWorker(), typeof document !== "undefined");
926
+ }
927
+ function javascriptExecuteRuntime(hasVm, hasWorker, hasDocument) {
799
928
  if (hasVm) return "vm";
929
+ if (hasWorker) return "worker";
800
930
  if (hasDocument) return "iframe";
801
- throw new Error("JavaScript execute needs node:vm or a document for the sandbox iframe.");
931
+ throw new Error("JavaScript execute needs node:vm, a browser worker sandbox, or a document for the sandbox iframe.");
932
+ }
933
+ function javascriptRuntimeProvenance(runtime) {
934
+ if (runtime === "vm") return {
935
+ host: "local",
936
+ runtime: "node:vm",
937
+ sandbox: "vm"
938
+ };
939
+ if (runtime === "worker") return {
940
+ host: "local",
941
+ runtime: "web-worker",
942
+ sandbox: "worker"
943
+ };
944
+ return {
945
+ host: "local",
946
+ runtime: "iframe",
947
+ sandbox: "srcdoc"
948
+ };
949
+ }
950
+ function withExecutionRuntime(error, runtime) {
951
+ if (error instanceof Error) return Object.assign(error, { executionRuntime: runtime });
952
+ if (isErrorLike(error)) return Object.assign(error, { executionRuntime: runtime });
953
+ return Object.assign(new Error(String(error)), { executionRuntime: runtime });
954
+ }
955
+ function executionRuntimeFromError(error) {
956
+ if (error && typeof error === "object" && "executionRuntime" in error && isJavaScriptExecutionRuntime(error.executionRuntime)) return error.executionRuntime;
957
+ }
958
+ function isJavaScriptExecutionRuntime(value) {
959
+ return value === "vm" || value === "worker" || value === "iframe";
802
960
  }
803
961
  function isTimeout(error) {
804
962
  return Boolean(error && typeof error === "object" && "code" in error && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT");
@@ -1065,9 +1223,12 @@ async function runTypeScript(request) {
1065
1223
  const stdio = new StdioBuffer(tracker.startedAt);
1066
1224
  tracker.start("compile", "Strip types");
1067
1225
  const javascript = stripTypeScript(request.code);
1226
+ const runtime = currentJavaScriptRuntime();
1227
+ let executedRuntime = runtime;
1068
1228
  tracker.start("execute", "Execute");
1069
1229
  try {
1070
- const value = await executeScript(javascript, request.timeoutMs, stdio, request.signal);
1230
+ const result = await executeScriptWithRuntime(javascript, request.timeoutMs, stdio, request.signal, runtime);
1231
+ executedRuntime = result.runtime;
1071
1232
  tracker.stop();
1072
1233
  return {
1073
1234
  status: "ok",
@@ -1078,14 +1239,10 @@ async function runTypeScript(request) {
1078
1239
  host: "local",
1079
1240
  runtime: "strip-types"
1080
1241
  },
1081
- execute: {
1082
- host: "local",
1083
- runtime: hasNodeVm() ? "node:vm" : "iframe",
1084
- sandbox: hasNodeVm() ? "vm" : "srcdoc"
1085
- }
1242
+ execute: javascriptRuntimeProvenance(executedRuntime)
1086
1243
  },
1087
1244
  timing: tracker.report(),
1088
- value: value === void 0 ? void 0 : String(value)
1245
+ value: result.value === void 0 ? void 0 : String(result.value)
1089
1246
  };
1090
1247
  } catch (error) {
1091
1248
  if (isAbortError(error) || request.signal?.aborted) throw error;
@@ -1105,11 +1262,7 @@ async function runTypeScript(request) {
1105
1262
  host: "local",
1106
1263
  runtime: "strip-types"
1107
1264
  },
1108
- execute: {
1109
- host: "local",
1110
- runtime: hasNodeVm() ? "node:vm" : "iframe",
1111
- sandbox: hasNodeVm() ? "vm" : "srcdoc"
1112
- }
1265
+ execute: javascriptRuntimeProvenance(executedRuntime)
1113
1266
  },
1114
1267
  timing: tracker.report()
1115
1268
  };
@@ -1282,9 +1435,12 @@ function errorMessage(error) {
1282
1435
  }
1283
1436
  function friendlyTransportMessage(error) {
1284
1437
  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.";
1438
+ 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
1439
  return message;
1287
1440
  }
1441
+ function transportFailureStatus(error) {
1442
+ return isOfflineError(error) ? "offline" : "error";
1443
+ }
1288
1444
  function errorResult(message, source = "code-play", status = "error") {
1289
1445
  return withStdioText({
1290
1446
  status,
@@ -1298,6 +1454,14 @@ function errorResult(message, source = "code-play", status = "error") {
1298
1454
  timing: emptyTiming()
1299
1455
  });
1300
1456
  }
1457
+ function isOfflineError(error) {
1458
+ const message = errorMessage(error);
1459
+ const name = error && typeof error === "object" && "name" in error ? String(error.name) : void 0;
1460
+ const typeError = error instanceof TypeError || name === "TypeError";
1461
+ if (name === "MissingTransportError") return true;
1462
+ if (typeError && /failed to fetch|networkerror|load failed|network request failed/i.test(message)) return true;
1463
+ return /\boffline\b|no code play transport|network request failed/i.test(message);
1464
+ }
1301
1465
  //#endregion
1302
1466
  //#region src/session.ts
1303
1467
  var CodePlaySession = class {
@@ -1317,6 +1481,7 @@ var CodePlaySession = class {
1317
1481
  timeoutMs;
1318
1482
  transport;
1319
1483
  endpoints;
1484
+ project;
1320
1485
  loadTypeScript;
1321
1486
  listeners = /* @__PURE__ */ new Map();
1322
1487
  abort;
@@ -1328,6 +1493,7 @@ var CodePlaySession = class {
1328
1493
  this.timeoutMs = input.timeoutMs;
1329
1494
  this.transport = input.transport;
1330
1495
  this.endpoints = input.endpoints;
1496
+ this.project = input.project;
1331
1497
  this.loadTypeScript = input.loadTypeScript;
1332
1498
  }
1333
1499
  on(event, listener) {
@@ -1368,6 +1534,7 @@ var CodePlaySession = class {
1368
1534
  transport: this.transport,
1369
1535
  loadTypeScript: this.loadTypeScript,
1370
1536
  endpoints: this.endpoints,
1537
+ project: this.project,
1371
1538
  signal
1372
1539
  };
1373
1540
  try {
@@ -1375,7 +1542,7 @@ var CodePlaySession = class {
1375
1542
  return this.finish(result);
1376
1543
  } catch (error) {
1377
1544
  if (signal.aborted || isAbortError(error)) return this.finish(errorResult("Run cancelled.", "code-play", "cancelled"));
1378
- return this.finish(errorResult(friendlyTransportMessage(error)));
1545
+ return this.finish(errorResult(friendlyTransportMessage(error), "code-play", transportFailureStatus(error)));
1379
1546
  }
1380
1547
  }
1381
1548
  finish(result) {
@@ -1460,62 +1627,221 @@ async function runPlayAction(input) {
1460
1627
  input.setBusy(false);
1461
1628
  }
1462
1629
  }
1630
+ function idleRunActionState() {
1631
+ return { phase: "idle" };
1632
+ }
1633
+ function runningRunActionState(action, startedAtMs = Date.now()) {
1634
+ return {
1635
+ phase: "running",
1636
+ action,
1637
+ startedAtMs
1638
+ };
1639
+ }
1640
+ function resultRunActionState(action, result, finishedAtMs = Date.now()) {
1641
+ return {
1642
+ phase: result.status === "offline" ? "offline" : result.status === "ok" || result.status === "cancelled" ? "result" : "error",
1643
+ action,
1644
+ result,
1645
+ message: result.diagnostics[0]?.message,
1646
+ finishedAtMs
1647
+ };
1648
+ }
1649
+ function errorRunActionState(action, error, finishedAtMs = Date.now()) {
1650
+ return {
1651
+ phase: "error",
1652
+ action,
1653
+ message: errorMessage(error),
1654
+ finishedAtMs
1655
+ };
1656
+ }
1463
1657
  //#endregion
1464
1658
  //#region src/styles.ts
1465
1659
  var CODE_PLAY_STYLES = `
1466
1660
  .ox-code-play {
1467
1661
  border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 16%, transparent));
1468
- border-radius: 12px;
1469
- background: var(--octc-color-bg-alt, var(--octc-color-bg, Canvas));
1662
+ border-radius: 6px;
1663
+ background: var(--octc-color-bg, Canvas);
1470
1664
  color: var(--octc-color-text, CanvasText);
1471
1665
  overflow: hidden;
1472
- margin: 1.25rem 0;
1666
+ margin: 1rem 0;
1473
1667
  }
1474
1668
  .ox-code-play__toolbar {
1475
1669
  display: flex;
1476
1670
  flex-wrap: wrap;
1477
- gap: 0.5rem;
1671
+ gap: 0.4rem;
1478
1672
  align-items: center;
1479
- padding: 0.6rem 0.8rem;
1673
+ padding: 0.45rem 0.6rem;
1480
1674
  border-bottom: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 12%, transparent));
1675
+ background: color-mix(in srgb, var(--octc-color-bg-alt, var(--octc-color-bg, Canvas)) 64%, transparent);
1676
+ }
1677
+ .ox-code-play__summary {
1678
+ display: inline-flex;
1679
+ flex: 1 1 12rem;
1680
+ min-width: 9rem;
1681
+ gap: 0.45rem;
1682
+ align-items: baseline;
1481
1683
  }
1482
1684
  .ox-code-play__lang {
1483
1685
  font: 600 0.8rem/1.2 ui-sans-serif, system-ui, sans-serif;
1484
- margin-right: auto;
1485
1686
  color: var(--octc-color-text, CanvasText);
1486
1687
  }
1688
+ .ox-code-play__title {
1689
+ min-width: 0;
1690
+ overflow-wrap: anywhere;
1691
+ font: 500 0.78rem/1.25 ui-sans-serif, system-ui, sans-serif;
1692
+ opacity: 0.72;
1693
+ }
1694
+ .ox-code-play__status {
1695
+ min-width: 4.6rem;
1696
+ text-align: center;
1697
+ border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 16%, transparent));
1698
+ border-radius: 999px;
1699
+ padding: 0.12rem 0.45rem;
1700
+ font: 650 0.68rem/1.25 ui-sans-serif, system-ui, sans-serif;
1701
+ color: var(--octc-color-text, CanvasText);
1702
+ background: var(--octc-color-bg, Canvas);
1703
+ }
1704
+ .ox-code-play[data-ox-run-state="running"] .ox-code-play__status {
1705
+ color: var(--octc-color-primary, var(--octc-accent, #4f46e5));
1706
+ }
1707
+ .ox-code-play[data-ox-run-state="error"] .ox-code-play__status,
1708
+ .ox-code-play[data-ox-run-state="offline"] .ox-code-play__status {
1709
+ color: var(--octc-danger, #b42318);
1710
+ }
1487
1711
  .ox-code-play__toolbar button {
1488
1712
  appearance: none;
1489
1713
  border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 20%, transparent));
1490
1714
  background: color-mix(in srgb, var(--octc-color-text, CanvasText) 8%, transparent);
1491
1715
  color: var(--octc-color-text, CanvasText);
1492
- border-radius: 999px;
1493
- padding: 0.25rem 0.75rem;
1716
+ border-radius: 6px;
1717
+ min-height: 1.9rem;
1718
+ padding: 0.18rem 0.62rem;
1494
1719
  font: 600 0.75rem/1.4 ui-sans-serif, system-ui, sans-serif;
1495
1720
  cursor: pointer;
1496
1721
  }
1497
1722
  .ox-code-play__toolbar button:disabled { opacity: 0.55; cursor: progress; }
1723
+ .ox-code-play__toolbar button:focus-visible,
1724
+ .ox-code-play__tabs button:focus-visible,
1725
+ .ox-code-play__field input:focus-visible,
1726
+ .ox-code-play__field select:focus-visible,
1727
+ .ox-code-play__panel:focus-visible {
1728
+ outline: 2px solid var(--octc-color-primary, var(--octc-accent, #4f46e5));
1729
+ outline-offset: 2px;
1730
+ }
1731
+ .ox-code-play__runtime {
1732
+ display: flex;
1733
+ flex-wrap: wrap;
1734
+ gap: 0.4rem 0.7rem;
1735
+ align-items: center;
1736
+ padding: 0.38rem 0.6rem;
1737
+ border-bottom: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 10%, transparent));
1738
+ background: color-mix(in srgb, var(--octc-color-bg-alt, var(--octc-color-bg, Canvas)) 56%, transparent);
1739
+ }
1740
+ .ox-code-play__runtime-chip {
1741
+ display: inline-flex;
1742
+ gap: 0.3rem;
1743
+ align-items: baseline;
1744
+ min-width: 0;
1745
+ padding: 0;
1746
+ border: 0;
1747
+ background: transparent;
1748
+ }
1749
+ .ox-code-play__runtime-chip span {
1750
+ font: 650 0.62rem/1.1 ui-sans-serif, system-ui, sans-serif;
1751
+ text-transform: uppercase;
1752
+ letter-spacing: 0.03em;
1753
+ opacity: 0.62;
1754
+ }
1755
+ .ox-code-play__runtime-chip strong {
1756
+ font: 650 0.72rem/1.2 ui-sans-serif, system-ui, sans-serif;
1757
+ }
1758
+ .ox-code-play__runtime-chip--ok strong { color: var(--octc-color-primary, var(--octc-accent, #4f46e5)); }
1759
+ .ox-code-play__runtime-chip--warn strong { color: var(--octc-warning, #b54708); }
1760
+ .ox-code-play__runtime-chip--muted strong { opacity: 0.72; }
1761
+ .ox-code-play__project {
1762
+ display: inline-flex;
1763
+ flex: 1 1 18rem;
1764
+ min-width: min(100%, 16rem);
1765
+ gap: 0.45rem;
1766
+ align-items: center;
1767
+ flex-wrap: wrap;
1768
+ margin-left: auto;
1769
+ padding: 0.28rem 0.42rem;
1770
+ border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 14%, transparent));
1771
+ border-radius: 6px;
1772
+ background: var(--octc-color-bg, Canvas);
1773
+ }
1774
+ .ox-code-play__project-main,
1775
+ .ox-code-play__project-entry {
1776
+ display: inline-grid;
1777
+ gap: 0.1rem;
1778
+ min-width: 0;
1779
+ }
1780
+ .ox-code-play__project-main { flex: 1 1 8.5rem; }
1781
+ .ox-code-play__project-entry { flex: 999 1 9rem; }
1782
+ .ox-code-play__project-main span,
1783
+ .ox-code-play__project-entry span {
1784
+ font: 600 0.62rem/1.1 ui-sans-serif, system-ui, sans-serif;
1785
+ text-transform: uppercase;
1786
+ letter-spacing: 0.04em;
1787
+ opacity: 0.62;
1788
+ }
1789
+ .ox-code-play__project-main strong,
1790
+ .ox-code-play__project-entry strong {
1791
+ min-width: 0;
1792
+ overflow-wrap: anywhere;
1793
+ font: 650 0.78rem/1.2 ui-sans-serif, system-ui, sans-serif;
1794
+ }
1795
+ .ox-code-play__project-files,
1796
+ .ox-code-play__project-warning,
1797
+ .ox-code-play__project-link {
1798
+ flex: 0 0 auto;
1799
+ border-radius: 6px;
1800
+ padding: 0.18rem 0.45rem;
1801
+ font: 650 0.68rem/1.25 ui-sans-serif, system-ui, sans-serif;
1802
+ }
1803
+ .ox-code-play__project-files {
1804
+ background: color-mix(in srgb, var(--octc-color-text, CanvasText) 8%, transparent);
1805
+ }
1806
+ .ox-code-play__project-warning {
1807
+ color: var(--octc-warning, #b54708);
1808
+ background: color-mix(in srgb, var(--octc-warning, #b54708) 12%, transparent);
1809
+ }
1810
+ .ox-code-play__project-link {
1811
+ color: var(--octc-color-primary, var(--octc-accent, #4f46e5));
1812
+ background: color-mix(in srgb, var(--octc-color-primary, var(--octc-accent, #4f46e5)) 12%, transparent);
1813
+ text-decoration: none;
1814
+ }
1815
+ .ox-code-play__project-link:focus-visible {
1816
+ outline: 2px solid var(--octc-color-primary, var(--octc-accent, #4f46e5));
1817
+ outline-offset: 2px;
1818
+ }
1498
1819
  .ox-code-play .ox-code { margin: 0; }
1499
- .ox-code-play__source pre { margin: 0; border: 0; border-radius: 0; }
1820
+ .ox-code-play__source pre {
1821
+ margin: 0;
1822
+ border: 0;
1823
+ border-radius: 0;
1824
+ padding: 0.75rem 0.8rem;
1825
+ }
1500
1826
  .ox-code-play__tabs {
1501
1827
  display: flex;
1502
- gap: 0.25rem;
1503
- padding: 0.4rem 0.7rem 0;
1828
+ gap: 0.15rem;
1829
+ padding: 0.35rem 0.55rem 0;
1504
1830
  }
1505
1831
  .ox-code-play__tabs button {
1506
1832
  appearance: none;
1507
1833
  border: 0;
1508
1834
  background: transparent;
1509
1835
  color: var(--octc-color-text, CanvasText);
1510
- padding: 0.35rem 0.55rem;
1511
- border-radius: 8px 8px 0 0;
1836
+ padding: 0.28rem 0.48rem;
1837
+ border-radius: 5px 5px 0 0;
1512
1838
  font: 600 0.75rem/1.2 ui-sans-serif, system-ui, sans-serif;
1513
1839
  cursor: pointer;
1514
1840
  }
1515
1841
  .ox-code-play__tabs button[aria-selected="true"] {
1516
1842
  background: color-mix(in srgb, var(--octc-color-text, CanvasText) 10%, transparent);
1517
1843
  }
1518
- .ox-code-play__panel { padding: 0.7rem 0.8rem 0.9rem; color: var(--octc-color-text, CanvasText); }
1844
+ .ox-code-play__panel { padding: 0.55rem 0.65rem 0.7rem; color: var(--octc-color-text, CanvasText); }
1519
1845
  .ox-code-play__panel pre {
1520
1846
  background: transparent !important;
1521
1847
  color: inherit !important;
@@ -1526,7 +1852,7 @@ var CODE_PLAY_STYLES = `
1526
1852
  }
1527
1853
  .ox-code-play__empty { margin: 0; opacity: 0.7; font-size: 0.85rem; }
1528
1854
  .ox-code-play__stdio { font: 12px/1.45 ui-monospace, SFMono-Regular, monospace; }
1529
- .ox-code-play__stdio-line { display: grid; grid-template-columns: 8.5rem 1fr; gap: 0.6rem; white-space: pre-wrap; }
1855
+ .ox-code-play__stdio-line { display: grid; grid-template-columns: 5.4rem 1fr; gap: 0.55rem; white-space: pre-wrap; }
1530
1856
  .ox-code-play__stdio-line--stderr { color: var(--octc-danger, #b42318); }
1531
1857
  .ox-code-play__stdio-line--stdin { opacity: 0.75; }
1532
1858
  .ox-code-play__stdio-meta { opacity: 0.6; }
@@ -1535,7 +1861,7 @@ var CODE_PLAY_STYLES = `
1535
1861
  .ox-code-play__field input, .ox-code-play__field select {
1536
1862
  font: inherit;
1537
1863
  padding: 0.3rem 0.45rem;
1538
- border-radius: 8px;
1864
+ border-radius: 6px;
1539
1865
  border: 1px solid var(--octc-color-border, color-mix(in srgb, currentColor 18%, transparent));
1540
1866
  background: var(--octc-color-bg, Canvas);
1541
1867
  color: var(--octc-color-text, CanvasText);
@@ -1557,6 +1883,20 @@ var CODE_PLAY_STYLES = `
1557
1883
  .ox-code-play__diag--warning { color: var(--octc-warning, #b54708); }
1558
1884
  .ox-code-play--compact .ox-code-play__tabs { display: none; }
1559
1885
  .ox-code-play--compact .ox-code-play__panel[data-panel]:not([data-panel="stdio"]):not([data-panel="stderr"]) { display: none; }
1886
+ @media (max-width: 640px) {
1887
+ .ox-code-play__toolbar {
1888
+ align-items: flex-start;
1889
+ }
1890
+ .ox-code-play__runtime,
1891
+ .ox-code-play__tabs,
1892
+ .ox-code-play__panel {
1893
+ padding-inline: 0.55rem;
1894
+ }
1895
+ .ox-code-play__stdio-line {
1896
+ grid-template-columns: 1fr;
1897
+ gap: 0.15rem;
1898
+ }
1899
+ }
1560
1900
  `;
1561
1901
  //#endregion
1562
1902
  //#region src/viewers.ts
@@ -1630,42 +1970,141 @@ function renderPlayUi(state) {
1630
1970
  if (preset === "headless") return "";
1631
1971
  const panel = state.panel ?? "stdio";
1632
1972
  const canTypecheck = state.payload.capabilities.typecheck;
1973
+ const runState = state.runState ?? (state.busy ? {
1974
+ phase: "running",
1975
+ action: "execute"
1976
+ } : idleRunActionState());
1977
+ const isBusy = Boolean(state.busy) || runState.phase === "running";
1633
1978
  const tabs = renderTabs(state, panel);
1634
1979
  const viewers = state.payload.viewers;
1635
- return `<div class="ox-code-play ox-code-play--${preset}" data-ox-code-play-ui>
1980
+ const label = widgetLabel(state.payload, definition);
1981
+ 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
1982
  <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>
1983
+ <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>
1984
+ <span class="ox-code-play__status" data-ox-status role="status" aria-live="polite">${renderRunStatusText(runState)}</span>
1985
+ <button type="button" data-ox-action="run" aria-label="${escapeHtml(`Run ${label}`)}"${actionButtonAttrs("run", Boolean(state.busy))}>Run</button>
1986
+ ${canTypecheck ? `<button type="button" data-ox-action="typecheck" aria-label="${escapeHtml(`Typecheck ${label}`)}"${actionButtonAttrs("typecheck", Boolean(state.busy))}>Typecheck</button>` : ""}
1987
+ <button type="button" data-ox-action="cancel" aria-label="${escapeHtml(`Cancel ${label}`)}"${actionButtonAttrs("cancel", Boolean(state.busy))}>Cancel</button>
1641
1988
  </div>
1989
+ ${renderRuntimeStrip(state.payload, definition)}
1642
1990
  <div class="ox-code-play__source"></div>
1643
1991
  ${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>
1992
+ ${viewers.stdio ? renderPanel("stdio", "Stdio", panel, preset, `${renderDiagnosticsHtml(state.result)}${renderStdioHtml(state.result?.stdio ?? [])}`) : ""}
1993
+ ${viewers.stderr ? renderPanel("stderr", "Stderr", panel, preset, renderStderrHtml(state.result)) : ""}
1994
+ ${viewers.config ? renderPanel("config", "Config", panel, preset, renderConfigHtml(definition?.configSchema ?? [], state.payload.config)) : ""}
1995
+ ${viewers.provenance ? renderPanel("provenance", "Provenance", panel, preset, renderProvenanceHtml(state.result?.provenance)) : ""}
1996
+ ${viewers.timing ? renderPanel("timing", "Timing", panel, preset, renderTimingHtml(state.result?.timing)) : ""}
1649
1997
  </div>`;
1650
1998
  }
1999
+ function renderRunStatusText(state) {
2000
+ if (state.phase === "running") return state.action === "typecheck" ? "Typechecking" : "Running";
2001
+ if (state.phase === "offline") return "Offline";
2002
+ if (state.phase === "error") {
2003
+ if (state.result?.status === "timeout") return "Timed out";
2004
+ if (state.result?.status === "unsupported") return "Unsupported";
2005
+ return "Error";
2006
+ }
2007
+ if (state.phase === "result") {
2008
+ if (state.result?.status === "cancelled") return "Cancelled";
2009
+ return "Done";
2010
+ }
2011
+ return "Ready";
2012
+ }
2013
+ function renderRuntimeStrip(payload, definition) {
2014
+ const runtime = runtimeLabel(payload, definition);
2015
+ const executor = executorLabel(payload, definition);
2016
+ const checks = payload.capabilities.typecheck ? "Typecheck ready" : "Run only";
2017
+ return `<div class="ox-code-play__runtime" aria-label="Code Play runtime">${[
2018
+ runtimeChip("Runtime", runtime.label, runtime.kind),
2019
+ runtimeChip("Executor", executor.label, executor.kind),
2020
+ runtimeChip("Checks", checks, payload.capabilities.typecheck ? "ok" : "muted")
2021
+ ].join("")}${renderProjectSandboxHtml(payload.project)}</div>`;
2022
+ }
2023
+ function runtimeLabel(payload, definition) {
2024
+ switch (definition?.backend) {
2025
+ case "javascript": return {
2026
+ label: "Browser sandbox",
2027
+ kind: "ok"
2028
+ };
2029
+ case "typescript": return {
2030
+ label: "TypeScript sandbox",
2031
+ kind: "ok"
2032
+ };
2033
+ case "framework": return {
2034
+ label: `${definition.name} iframe preview`,
2035
+ kind: "ok"
2036
+ };
2037
+ case "rust-playground": return {
2038
+ label: "Rust Playground",
2039
+ kind: payload.endpoints?.rust ? "ok" : "warn"
2040
+ };
2041
+ case "go-playground": return {
2042
+ label: "Go Playground",
2043
+ kind: payload.endpoints?.go ? "ok" : "warn"
2044
+ };
2045
+ case "remote": return payload.endpoint ? {
2046
+ label: "Piston-compatible",
2047
+ kind: "ok"
2048
+ } : {
2049
+ label: "Endpoint missing",
2050
+ kind: "warn"
2051
+ };
2052
+ default: return {
2053
+ label: definition?.name ?? payload.language,
2054
+ kind: "muted"
2055
+ };
2056
+ }
2057
+ }
2058
+ function executorLabel(payload, definition) {
2059
+ if (!payload.capabilities.execute) return {
2060
+ label: "Disabled",
2061
+ kind: "warn"
2062
+ };
2063
+ if (definition?.backend === "remote" && !payload.endpoint) return {
2064
+ label: "Configure endpoint",
2065
+ kind: "warn"
2066
+ };
2067
+ return {
2068
+ label: "On demand",
2069
+ kind: "ok"
2070
+ };
2071
+ }
2072
+ function runtimeChip(label, value, kind) {
2073
+ return `<span class="ox-code-play__runtime-chip ox-code-play__runtime-chip--${kind}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></span>`;
2074
+ }
2075
+ function renderProjectSandboxHtml(project) {
2076
+ if (!project) return "";
2077
+ const fileCount = `${project.files.length} ${project.files.length === 1 ? "file" : "files"}`;
2078
+ const target = project.target === "browser" ? "Browser project" : project.target === "node" ? "Node-like project" : "External project";
2079
+ const url = project.openUrl ?? project.fallbackUrl;
2080
+ const warnings = project.warnings?.length ? `<span class="ox-code-play__project-warning" title="${escapeAttribute(project.warnings.join("\n"))}">Warnings</span>` : "";
2081
+ const link = url ? `<a class="ox-code-play__project-link" href="${escapeAttribute(url)}" target="_blank" rel="noopener noreferrer">Open</a>` : "";
2082
+ return `<span class="ox-code-play__project" data-ox-project-provider="${escapeAttribute(project.provider)}">
2083
+ <span class="ox-code-play__project-main"><span>${escapeHtml(project.label)}</span><strong>${escapeHtml(target)}</strong></span>
2084
+ ${project.entry ? `<span class="ox-code-play__project-entry"><span>Entry</span><strong>${escapeHtml(project.entry)}</strong></span>` : ""}
2085
+ <span class="ox-code-play__project-files">${escapeHtml(fileCount)}</span>
2086
+ ${warnings}${link}
2087
+ </span>`;
2088
+ }
1651
2089
  function renderTabs(state, panel) {
1652
2090
  if (state.payload.ui === "compact") return "";
1653
2091
  const viewers = state.payload.viewers;
1654
2092
  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) : ""
2093
+ viewers.stdio ? tab("stdio", "Stdio", panel) : "",
2094
+ viewers.stderr ? tab("stderr", "Stderr", panel) : "",
2095
+ viewers.config ? tab("config", "Config", panel) : "",
2096
+ viewers.provenance ? tab("provenance", "Provenance", panel) : "",
2097
+ viewers.timing ? tab("timing", "Timing", panel) : ""
1660
2098
  ].filter(Boolean).join("");
1661
2099
  return buttons ? `<div class="ox-code-play__tabs" role="tablist">${buttons}</div>` : "";
1662
2100
  }
1663
2101
  function tab(id, label, selected) {
1664
- return `<button type="button" role="tab" data-ox-panel="${id}" aria-selected="${selected === id ? "true" : "false"}">${label}</button>`;
2102
+ const isSelected = selected === id;
2103
+ return `<button type="button" role="tab" data-ox-panel="${id}" aria-selected="${isSelected ? "true" : "false"}" tabindex="${isSelected ? "0" : "-1"}">${label}</button>`;
1665
2104
  }
1666
2105
  function actionButtonAttrs(action, busy) {
1667
2106
  const state = actionBusyState(action, busy);
1668
- return `${state.disabled ? " disabled" : ""}${state.hidden ? " hidden" : ""}`;
2107
+ return `${state.disabled ? " disabled aria-disabled=\"true\"" : ""}${state.hidden ? " hidden" : ""}`;
1669
2108
  }
1670
2109
  function actionBusyState(action, busy) {
1671
2110
  if (action === "cancel") return {
@@ -1682,15 +2121,30 @@ function applyActionBusy(root, busy) {
1682
2121
  const state = actionBusyState(button.dataset.oxAction ?? "", busy);
1683
2122
  button.disabled = state.disabled;
1684
2123
  button.hidden = state.hidden;
2124
+ button.setAttribute("aria-disabled", state.disabled ? "true" : "false");
1685
2125
  }
2126
+ for (const widget of queryPlayWidgets(root)) widget.setAttribute("aria-busy", busy ? "true" : "false");
2127
+ }
2128
+ function renderPanel(id, label, current, preset, html) {
2129
+ 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
2130
  }
1687
2131
  function hidden(current, id, preset) {
1688
2132
  if (preset === "compact" && (id === "stdio" || id === "stderr")) return "";
1689
2133
  return current === id ? "" : " hidden";
1690
2134
  }
2135
+ function widgetLabel(payload, definition) {
2136
+ const language = definition?.name ?? payload.language;
2137
+ return payload.title ? `${payload.title} (${language})` : `${language} Code Play`;
2138
+ }
2139
+ function queryPlayWidgets(root) {
2140
+ const widgets = [...root.querySelectorAll(".ox-code-play")];
2141
+ if (typeof HTMLElement !== "undefined" && root instanceof HTMLElement && root.matches(".ox-code-play")) widgets.unshift(root);
2142
+ return widgets;
2143
+ }
1691
2144
  //#endregion
1692
2145
  //#region src/hydrate.ts
1693
2146
  var STYLE_ID = "ox-code-play-styles";
2147
+ var widgetId = 0;
1694
2148
  function hydrateCodePlay(root = defaultRoot(), options = {}) {
1695
2149
  ensureStyles();
1696
2150
  const client = options.client ?? createCodePlayFromPayloads(root);
@@ -1703,7 +2157,7 @@ function mountCodePlay(element, options = {}) {
1703
2157
  const payload = readPlayPayload(element.getAttribute("data-ox-code-play") ?? "");
1704
2158
  if (!payload || payload.ui === "headless") return;
1705
2159
  const client = options.client ?? createCodePlay({
1706
- languages: { [payload.language]: true },
2160
+ languages: { [payload.language]: languageEnableFromPayload(payload) },
1707
2161
  endpoints: payload.endpoints
1708
2162
  });
1709
2163
  const source = element.innerHTML;
@@ -1715,14 +2169,17 @@ function mountCodePlay(element, options = {}) {
1715
2169
  if (sourceSlot) sourceSlot.innerHTML = source;
1716
2170
  element.replaceChildren(widget);
1717
2171
  element.dataset.oxCodePlayMounted = "true";
2172
+ element.removeAttribute("inert");
1718
2173
  bindWidget(element, payload, client);
2174
+ prepareA11y(element);
1719
2175
  }
1720
2176
  function bindWidget(element, payload, client) {
1721
2177
  let current = payload;
1722
2178
  const session = client.createSession({
1723
2179
  language: payload.language,
1724
2180
  code: payload.code,
1725
- config: payload.config
2181
+ config: payload.config,
2182
+ project: payload.project
1726
2183
  });
1727
2184
  const runButton = element.querySelector("[data-ox-action=\"run\"]");
1728
2185
  const checkButton = element.querySelector("[data-ox-action=\"typecheck\"]");
@@ -1730,6 +2187,9 @@ function bindWidget(element, payload, client) {
1730
2187
  runButton?.addEventListener("click", () => void run("execute"));
1731
2188
  checkButton?.addEventListener("click", () => void run("typecheck"));
1732
2189
  cancelButton?.addEventListener("click", () => session.cancel());
2190
+ element.addEventListener("keydown", (event) => {
2191
+ if (event.target instanceof HTMLElement && event.target.matches("[role=\"tab\"]")) handleTabKeydown(element, event);
2192
+ });
1733
2193
  element.addEventListener("click", (event) => {
1734
2194
  const target = event.target;
1735
2195
  if (!(target instanceof HTMLElement)) return;
@@ -1748,13 +2208,20 @@ function bindWidget(element, payload, client) {
1748
2208
  async function run(action) {
1749
2209
  await runPlayAction({
1750
2210
  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)))
2211
+ setBusy: (busy) => {
2212
+ applyActionBusy(element, busy);
2213
+ if (busy) paintRunState(element, runningRunActionState(action));
2214
+ },
2215
+ onResult: (result) => paintResult(element, current, result, action),
2216
+ onError: (error) => {
2217
+ paintRunState(element, errorRunActionState(action, error));
2218
+ paintResult(element, current, errorResult(errorMessage(error)), action);
2219
+ }
1754
2220
  });
1755
2221
  }
1756
2222
  }
1757
- function paintResult(element, _payload, result) {
2223
+ function paintResult(element, _payload, result, action) {
2224
+ paintRunState(element, resultRunActionState(action, result));
1758
2225
  const stdio = element.querySelector("[data-panel=\"stdio\"]");
1759
2226
  if (stdio) {
1760
2227
  stdio.innerHTML = `${renderDiagnosticsHtml(result)}${renderStdioHtml(result.stdio)}`;
@@ -1777,6 +2244,13 @@ function paintResult(element, _payload, result) {
1777
2244
  if (stderr) stderr.innerHTML = renderStderrHtml(result);
1778
2245
  showPanel(element, resultPanelToShow(result, Boolean(stderr)));
1779
2246
  }
2247
+ function paintRunState(element, state) {
2248
+ const widget = element.querySelector(".ox-code-play");
2249
+ widget?.setAttribute("data-ox-run-state", state.phase);
2250
+ widget?.setAttribute("aria-busy", state.phase === "running" ? "true" : "false");
2251
+ const status = element.querySelector("[data-ox-status]");
2252
+ if (status) status.textContent = renderRunStatusText(state);
2253
+ }
1780
2254
  function resultPanelToShow(result, hasStderrPanel) {
1781
2255
  if (!hasStderrPanel) return "stdio";
1782
2256
  if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error") || result.status !== "ok" && Boolean(result.stderr)) return "stderr";
@@ -1784,7 +2258,11 @@ function resultPanelToShow(result, hasStderrPanel) {
1784
2258
  }
1785
2259
  function showPanel(element, panel) {
1786
2260
  const compact = Boolean(element.querySelector(".ox-code-play--compact"));
1787
- for (const tab of element.querySelectorAll("[data-ox-panel]")) tab.setAttribute("aria-selected", tab.getAttribute("data-ox-panel") === panel ? "true" : "false");
2261
+ for (const tab of element.querySelectorAll("[data-ox-panel]")) {
2262
+ const selected = tab.getAttribute("data-ox-panel") === panel;
2263
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
2264
+ tab.tabIndex = selected ? 0 : -1;
2265
+ }
1788
2266
  for (const node of element.querySelectorAll(".ox-code-play__panel")) {
1789
2267
  const id = node.dataset.panel;
1790
2268
  if (compact && (id === "stdio" || id === "stderr")) {
@@ -1794,6 +2272,38 @@ function showPanel(element, panel) {
1794
2272
  node.hidden = id !== panel;
1795
2273
  }
1796
2274
  }
2275
+ function handleTabKeydown(element, event) {
2276
+ if (![
2277
+ "ArrowLeft",
2278
+ "ArrowRight",
2279
+ "Home",
2280
+ "End"
2281
+ ].includes(event.key)) return;
2282
+ const tabs = [...element.querySelectorAll("[data-ox-panel]")];
2283
+ const current = tabs.indexOf(event.target);
2284
+ if (current === -1) return;
2285
+ event.preventDefault();
2286
+ 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];
2287
+ const panel = next?.dataset.oxPanel;
2288
+ if (!next || !panel) return;
2289
+ showPanel(element, panel);
2290
+ next.focus();
2291
+ }
2292
+ function prepareA11y(element) {
2293
+ const widget = element.querySelector(".ox-code-play");
2294
+ if (!widget) return;
2295
+ if (!widget.id) widget.id = `ox-code-play-${++widgetId}`;
2296
+ for (const tab of element.querySelectorAll("[data-ox-panel]")) {
2297
+ const panelName = tab.dataset.oxPanel;
2298
+ const panel = panelName ? element.querySelector(`[data-panel="${panelName}"]`) : null;
2299
+ if (!panelName || !panel) continue;
2300
+ tab.id ||= `${widget.id}-${panelName}-tab`;
2301
+ panel.id ||= `${widget.id}-${panelName}-panel`;
2302
+ tab.setAttribute("aria-controls", panel.id);
2303
+ panel.setAttribute("aria-labelledby", tab.id);
2304
+ panel.removeAttribute("aria-label");
2305
+ }
2306
+ }
1797
2307
  function readForm(form) {
1798
2308
  const data = new FormData(form);
1799
2309
  const config = {};
@@ -1806,7 +2316,7 @@ function createCodePlayFromPayloads(root) {
1806
2316
  let endpoints;
1807
2317
  for (const element of queryWidgets(root)) try {
1808
2318
  const payload = decodePayload(element.getAttribute("data-ox-code-play") ?? "");
1809
- languages[payload.language] = true;
2319
+ languages[payload.language] = languageEnableFromPayload(payload);
1810
2320
  endpoints = payload.endpoints ?? endpoints;
1811
2321
  } catch {}
1812
2322
  return createCodePlay({
@@ -1814,6 +2324,15 @@ function createCodePlayFromPayloads(root) {
1814
2324
  endpoints
1815
2325
  });
1816
2326
  }
2327
+ function languageEnableFromPayload(payload) {
2328
+ const enable = {
2329
+ execute: payload.capabilities.execute,
2330
+ typecheck: payload.capabilities.typecheck,
2331
+ config: payload.config
2332
+ };
2333
+ if (payload.endpoint) enable.endpoint = payload.endpoint;
2334
+ return enable;
2335
+ }
1817
2336
  function queryWidgets(root) {
1818
2337
  return [...root.querySelectorAll("[data-ox-code-play]")];
1819
2338
  }