Merge branch 'add_codesandbox' into 'development'

Add codesandbox

See merge request GeneralProtocols/xo/stack!51
This commit is contained in:
Jim Hamill
2025-04-07 05:55:06 +00:00
9 changed files with 866 additions and 29 deletions
+29 -25
View File
@@ -332,7 +332,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -342,7 +341,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -352,7 +350,6 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.3"
@@ -368,7 +365,6 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@@ -1469,7 +1465,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -3936,7 +3931,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
@@ -3950,7 +3944,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.13",
@@ -3961,7 +3954,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
@@ -3979,7 +3971,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.13",
@@ -4026,7 +4017,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.13"
@@ -4036,7 +4026,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.13",
@@ -4047,7 +4036,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.13",
@@ -4060,7 +4048,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.13",
@@ -4074,7 +4061,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@vueuse/core": {
@@ -4355,6 +4341,13 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/ace-builds": {
"version": "1.39.1",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.39.1.tgz",
"integrity": "sha512-HcJbBzx8qY66t9gZo/sQu7pi0wO/CFLdYn1LxQO1WQTfIkMfyc7LRnBpsp/oNCSSU/LL83jXHN1fqyOTuIhUjg==",
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -5515,7 +5508,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/cytoscape": {
@@ -6725,7 +6717,6 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -7121,7 +7112,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/esutils": {
@@ -9452,7 +9442,6 @@
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
@@ -10492,7 +10481,6 @@
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -11411,7 +11399,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -11564,7 +11551,6 @@
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -12321,6 +12307,12 @@
"node": ">=10.13.0"
}
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.9",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
@@ -12885,7 +12877,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -13763,7 +13754,7 @@
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -14309,7 +14300,6 @@
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.13",
@@ -14327,6 +14317,19 @@
}
}
},
"node_modules/vue3-ace-editor": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/vue3-ace-editor/-/vue3-ace-editor-2.2.4.tgz",
"integrity": "sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==",
"license": "MIT",
"dependencies": {
"resize-observer-polyfill": "^1.5.1"
},
"peerDependencies": {
"ace-builds": "*",
"vue": "^3"
}
},
"node_modules/walk-up-path": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz",
@@ -14717,7 +14720,8 @@
"license": "ISC",
"dependencies": {
"@xocash/primitives": "*",
"@xocash/stack": "*"
"@xocash/stack": "*",
"vue3-ace-editor": "^2.2.4"
},
"devDependencies": {
"typedoc": "^0.27.5",
+2 -1
View File
@@ -15,7 +15,8 @@
"license": "ISC",
"dependencies": {
"@xocash/primitives": "*",
"@xocash/stack": "*"
"@xocash/stack": "*",
"vue3-ace-editor": "^2.2.4"
},
"devDependencies": {
"typedoc": "^0.27.5",
@@ -0,0 +1,462 @@
<template>
<div class="code-sandbox" :style="sandboxStyle">
<div class="editor-container" :style="editorContainerStyle">
<v-ace-editor
v-model:value="code"
:style="editorStyle"
lang="javascript"
:theme="isDark ? 'github_dark' : 'github'"
@init="editorInit"
:options="{
showGutter: true,
minLines: 1,
maxLines: props.fullHeight ? null : 20,
showPrintMargin: false
}"
/>
<div class="editor-buttons">
<button v-if="!hideRunButton" @click="executeCode" class="primary-button">Run Code</button>
</div>
</div>
<div v-if="showLogs || fullHeight" class="output-container" :style="outputContainerStyle">
<div class="output-container-buttons">
<button v-if="!fullHeight" @click="showLogs = false" class="secondary-button">Close</button>
</div>
<div class="console-output" :style="consoleOutputStyle">
<div v-for="(log, index) in logs" :key="index" :class="['log-entry', log.type]">
<span class="log-prefix">[{{ log.type }}]:</span> {{ log.content }}
</div>
<div v-if="logs.length === 0" class="empty-state">
No console output yet. Run your code to see results here.
</div>
</div>
</div>
<div class="iframe-container" style="display:none">
<iframe
ref="sandboxIframe"
sandbox="allow-scripts allow-same-origin allow-modals allow-forms allow-popups"
allow="accelerometer; camera; encrypted-media; geolocation; gyroscope; microphone; midi"
class="sandbox-iframe"
></iframe>
</div>
<!-- Hidden slot to capture the default code -->
<!-- We need this because we want to place our code in slots -->
<div ref="codeRef" style="display: none;">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { useData } from 'vitepress';
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
import { VAceEditor } from 'vue3-ace-editor';
import ace from 'ace-builds';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-github_dark';
import 'ace-builds/src-noconflict/mode-javascript';
// Import the iframe HTML template
// NOTE: Having this in an external file is many-fold neater than stashing it all in here.
import iframeTemplateHtml from './CodeSandboxTemplate.html?raw';
const { isDark } = useData();
// Define props for the component
const props = defineProps({
enableNetwork: {
type: Boolean,
default: true
},
fullHeight: {
type: Boolean,
default: false
},
height: {
type: String,
default: 'auto'
},
editorHeight: {
type: String,
default: 'auto'
},
outputHeight: {
type: String,
default: 'auto'
},
hideRunButton: {
type: Boolean,
default: false,
}
});
// Define types for console logs
interface ConsoleLog {
type: 'log' | 'info' | 'warn' | 'error';
content: string;
}
// Define share URL base
const SHARE_BASE_URL = 'https://stack.xo.cash/playground.html';
// Define a unique ID to pass into the IFrame.
// NOTE: We do this so that we can reliably identify where the postMessage came from.
// If we do not do this and have multiple instances of CodeSandbox, then they will cross-chatter.
const thisSandboxID = generateSandboxId();
// State
const codeRef = ref(null);
const code = ref<string>('');
const logs = ref<ConsoleLog[]>([]);
const theme = ref('github_dark');
const showLogs = ref(false);
const sandboxIframe = ref<HTMLIFrameElement | null>(null);
// Computed styles based on props
const sandboxStyle = computed(() => ({
height: props.fullHeight ? '100%' : props.height,
display: 'flex',
flexDirection: 'column'
}));
const editorContainerStyle = computed(() => ({
height: props.fullHeight ? props.editorHeight !== 'auto' ? props.editorHeight : '50%' : 'auto',
flex: props.fullHeight && props.editorHeight === 'auto' ? '1 0 auto' : 'none',
display: 'flex',
flexDirection: 'column',
position: 'relative',
overflow: 'hidden' // Ensure overflowing content doesn't spill out
}));
const editorStyle = computed(() => ({
width: '100%',
height: '100%', // Fill container height
minHeight: props.fullHeight ? '0' : '3em'
}));
const outputContainerStyle = computed(() => ({
height: props.fullHeight ? props.outputHeight !== 'auto' ? props.outputHeight : 'auto' : 'auto',
flex: props.fullHeight && props.outputHeight === 'auto' ? '1 0 auto' : 'none',
display: 'flex',
flexDirection: 'column'
}));
const consoleOutputStyle = computed(() => ({
flex: props.fullHeight ? '1 1 auto' : 'none',
minHeight: props.fullHeight ? '0' : '120px',
maxHeight: props.fullHeight ? 'none' : '250px'
}));
// Methods
const clearLogs = () => {
logs.value = [];
};
function editorInit(editor: any) {
editor.session.setTabSize(2);
editor.renderer.setScrollMargin(5, 5, 0, 0);
// Make sure the editor properly reflects its container size
editor.renderer.updateFull();
}
// Generate a shareable URL with the current code
const shareCode = () => {
try {
// Convert the code to Base64 to make it more compact
// First, we get a UTF-8 string, then encode to Base64
const base64Code = btoa(unescape(encodeURIComponent(code.value)));
// Generate the shareable URL with the Base64 encoded content in the hash fragment
const shareUrl = `${SHARE_BASE_URL}#code=${encodeURIComponent(base64Code)}`;
// Check URL length - hash fragments can be longer, but still have limits
if (shareUrl.length > 10000) {
logs.value.push({
type: 'warn',
content: `URL is ${shareUrl.length} characters long, which is extremely long and may not work in some environments. Consider shortening your code.`
});
} else if (shareUrl.length > 4000) {
logs.value.push({
type: 'info',
content: `URL is ${shareUrl.length} characters long. While this should work in most modern browsers, it might be problematic in some environments.`
});
}
// Copy to clipboard
navigator.clipboard.writeText(shareUrl)
.then(() => {
logs.value.push({
type: 'info',
content: 'Shareable URL copied to clipboard!'
});
showLogs.value = true;
})
.catch(err => {
console.error('Failed to copy URL: ', err);
logs.value.push({
type: 'error',
content: 'Failed to copy URL to clipboard. URL is: ' + shareUrl
});
showLogs.value = true;
});
} catch (error) {
logs.value.push({
type: 'error',
content: 'Failed to generate shareable URL: ' + error
});
showLogs.value = true;
}
};
// Create HTML content for the iframe by replacing the placeholder with user code
const createIframeContent = (sandboxID: string, userCode: string): string => {
return iframeTemplateHtml.replace('__userCode__;', userCode).replaceAll('__sandboxID__', sandboxID);
};
function generateSandboxId(length: number = 6): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
// Execute the code in the iframe
const executeCode = () => {
clearLogs();
showLogs.value = true;
if (sandboxIframe.value) {
// Add an allow-scripts attribute dynamically if network is enabled
if (props.enableNetwork) {
sandboxIframe.value.sandbox.add('allow-scripts');
sandboxIframe.value.sandbox.add('allow-same-origin');
sandboxIframe.value.sandbox.add('allow-modals');
sandboxIframe.value.sandbox.add('allow-forms');
sandboxIframe.value.sandbox.add('allow-popups');
}
// Create the HTML content
const iframeContent = createIframeContent(thisSandboxID, code.value);
// Create a Blob containing the HTML content
const blob = new Blob([iframeContent], { type: 'text/html' });
// Create a URL for the Blob
const blobUrl = URL.createObjectURL(blob);
// Set the iframe's src to the Blob URL
sandboxIframe.value.src = blobUrl;
// Clean up the Blob URL after the iframe has loaded
sandboxIframe.value.onload = () => {
URL.revokeObjectURL(blobUrl);
// Log info about module support
if (props.enableNetwork) {
logs.value.push({
type: 'info',
content: 'ESM imports enabled - you can use external libraries'
});
}
};
// Handle load errors
sandboxIframe.value.onerror = () => {
logs.value.push({
type: 'error',
content: 'Failed to load sandbox environment'
});
};
}
};
// Handle messages from the iframe
const handleMessage = (event: MessageEvent) => {
const { sandboxID, type, content } = event.data;
// Make sure the message has the expected format
if (sandboxID && type && content) {
// Make sure the messsage was targetting this Sandbox specifically.
if(sandboxID !== thisSandboxID) {
return;
}
logs.value.push({
type: type as 'log' | 'info' | 'warn' | 'error',
content
});
}
};
// Lifecycle hooks
onMounted(() => {
// Check if there's code in the URL hash fragment
if (window.location.hash && window.location.hash.startsWith('#code=')) {
const codeParam = window.location.hash.substring(6); // Remove '#code='
// If code exists in URL hash, use it
try {
// Always assume Base64 encoding
code.value = decodeURIComponent(escape(atob(decodeURIComponent(codeParam))));
} catch (error) {
console.error('Failed to decode URL hash parameter:', error);
logs.value.push({
type: 'error',
content: 'Failed to decode URL hash parameter: ' + error
});
// Fallback to slot content if URL decoding fails
loadCodeFromSlot();
}
} else {
// Otherwise check query parameters (for backward compatibility)
const urlParams = new URLSearchParams(window.location.search);
const queryCodeParam = urlParams.get('code');
if (queryCodeParam) {
try {
// Try to decode from Base64
code.value = decodeURIComponent(escape(atob(decodeURIComponent(queryCodeParam))));
} catch (error) {
console.error('Failed to decode URL query parameter:', error);
// Fallback to slot content
loadCodeFromSlot();
}
} else {
// No code in hash or query, load from slot
loadCodeFromSlot();
}
}
// Add event listener for messages from the iframe
window.addEventListener('message', handleMessage);
});
// Function to load code from slot
const loadCodeFromSlot = () => {
if (codeRef.value) {
const template = codeRef.value;
if (template) {
code.value = template.innerText.trim();
}
}
};
onBeforeUnmount(() => {
// Clean up event listener
window.removeEventListener('message', handleMessage);
});
// Expose our functions so that they can be called from parent components.
defineExpose({
executeCode,
shareCode
});
</script>
<style>
.code-sandbox {
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
margin-bottom: 1em;
}
.editor-container {
position: relative;
background: var(--vp-code-block-bg);
padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden; /* Contain overflow */
}
.editor-container .ace_editor {
background: transparent !important;
font-family: var(--vp-font-family-mono);
line-height: var(--vp-code-line-height);
font-size: var(--vp-code-font-size);
flex: 1 1 auto; /* Allow editor to grow */
overflow: auto; /* Enable scrolling */
min-height: 3em;
}
.editor-container .ace_editor .ace_gutter {
background: transparent !important;
}
.editor-buttons {
position: absolute;
padding:10px;
bottom: 0px;
right: 0px;
display: flex;
gap: 8px;
}
.output-container {
position: relative;
background-color: var(--vp-code-block-bg);
border-top: 1px solid var(--vp-button-brand-bg);
}
.output-container-buttons {
position: absolute;
padding:10px;
top: 0px;
right: 0px;
}
.console-output {
min-height: 120px;
max-height: 250px;
overflow-y: auto;
padding: 8px;
font-family: var(--vp-font-family-mono);
line-height: var(--vp-code-line-height);
font-size: 12px;
}
.log-entry {
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.log {
color: var(--vp-c-text-1);
}
.info {
color: #0066cc;
}
.warn {
color: #ff9900;
background-color: rgba(255, 153, 0, 0.1);
}
.error {
color: #cc0000;
background-color: rgba(204, 0, 0, 0.1);
}
.log-prefix {
font-weight: bold;
}
.empty-state {
color: #999;
font-style: italic;
text-align: center;
padding: 24px 0;
}
.sandbox-iframe {
width: 0;
height: 0;
border: 0;
}
</style>
@@ -0,0 +1,122 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Code Sandbox</title>
<script>
// Override console methods to capture and post messages to parent
const originalConsole = window.console;
// Define console overrides
window.console = {
log: function (...args) {
originalConsole.log(...args);
window.parent.postMessage(
{
sandboxID: '__sandboxID__',
type: 'log',
content: args
.map((arg) =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg),
)
.join(' '),
},
'*',
);
},
info: function (...args) {
originalConsole.info(...args);
window.parent.postMessage(
{
sandboxID: '__sandboxID__',
type: 'info',
content: args
.map((arg) =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg),
)
.join(' '),
},
'*',
);
},
warn: function (...args) {
originalConsole.warn(...args);
window.parent.postMessage(
{
sandboxID: '__sandboxID__',
type: 'warn',
content: args
.map((arg) =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg),
)
.join(' '),
},
'*',
);
},
error: function (...args) {
originalConsole.error(...args);
window.parent.postMessage(
{
sandboxID: '__sandboxID__',
type: 'error',
content: args
.map((arg) =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg),
)
.join(' '),
},
'*',
);
},
};
// Catch global errors
window.onerror = function (message, source, lineno, colno, error) {
window.parent.postMessage(
{
sandboxID: '__sandboxID__',
type: 'error',
content: 'Error: ' + message,
},
'*',
);
return true; // Prevents the error from appearing in the iframe's console
};
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', function (event) {
window.parent.postMessage(
{
type: 'error',
content: 'Unhandled Promise Rejection: ' + event.reason,
},
'*',
);
});
</script>
</head>
<body>
<!-- The code runs in this sandbox environment -->
<!-- Module script for ESM support -->
<script type="module">
// Helper function to handle both dynamic and static imports
async function runUserCode() {
try {
// Execute the user code
__userCode__;
} catch (error) {
console.error(error.message);
}
}
runUserCode().catch((error) => {
console.error('Error in user code execution:', error.message);
});
</script>
</body>
</html>
@@ -0,0 +1,104 @@
<template>
<div id="playground">
<div class="top">
<div class="logo">
<img class="light-only" src="/logo-light.svg" alt="XO Stack Logo">
<img class="dark-only" src="/logo-dark.svg" alt="XO Stack Logo">
XO Stack - Playground
</div>
<div class="button-container">
<button class="primary-button" @click="shareCode">Share</button>
<button class="primary-button" @click="executeCode">Run Code</button>
</div>
</div>
<CodeSandbox
ref="sandbox"
height="100vh"
editorHeight="80%"
outputHeight="20%"
fullHeight
hideRunButton
>
<slot></slot>
</CodeSandbox>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useData } from 'vitepress';
import CodeSandbox from '@theme/components/CodeSandbox.vue'
const { isDark } = useData();
const sandbox = ref<InstanceType<typeof CodeSandbox> | null>(null);
async function executeCode() {
sandbox.value.executeCode();
}
async function shareCode() {
sandbox.value.shareCode()
}
</script>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
}
#playground {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.top {
display: flex;
align-items: center;
padding: 10px;
background-color: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-button-brand-bg);
}
.logo {
display: flex; align-items: center;
font-size: 16px;
margin-right: auto;
font-family: var(--vp-font-family-base);
}
.logo img {
margin-right: 10px;
height: 50px;
}
.row {
display: flex;
flex: 1;
overflow: hidden;
}
.left {
width: 80%;
height: 100%;
border-right: 1px solid #ddd;
}
.right {
width: 20%;
height: 100%;
overflow: auto;
padding: 10px;
box-sizing: border-box;
background-color: #1e1e1e;
color: #ddd;
font-family: 'Consolas', monospace;
}
#monaco-editor-container {
width: 100%;
height: 100%;
}
.console-output {
white-space: pre-wrap;
}
</style>
@@ -130,6 +130,41 @@ details summary {
gap: 20px; /* Space between the icons and arrows */
}
/* Buttons */
.button-container {
display: flex; /* or display: grid */
gap: 10px; /* Adjust the size to your preference */
}
.primary-button {
padding: 8px 16px;
background-color: var(--vp-button-brand-bg);
color: var(--vp-button-brand-text);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.primary-button:hover {
background-color: var(--vp-button-brand-hover-bg);
}
.secondary-button {
padding: 4px 8px;
background-color: var(--vp-c-brand-soft);
color: var(--vp-button-brand-text);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.secondary-button:hover {
background-color: var(--vp-button-brand-hover-bg);
}
/* BCH icon styling */
.bch-icon {
width: 64px;
+1 -3
View File
@@ -9,7 +9,5 @@ import './tools.css';
export default {
extends: DefaultTheme,
Layout,
enhanceApp({ app, router, siteData }) {
// ...
},
enhanceApp({ _app, _router, _siteData }) {},
} satisfies Theme;
+30
View File
@@ -0,0 +1,30 @@
---
layout: false
---
<script lang="ts" setup>
// NOTE: We cannot just inline Playground HTML in this file.
// VitePress has its own pre-processing of Vue templates in .md files and will break it.
// So we have to define the playground as a component and import it.
import Playground from '@theme/components/Playground.vue'
</script>
<!-- TODO: Create a cooler demo once Stack is more stable -->
<Playground>
```
// Import the Electrum Blockchain Adapter from XO Stack.
const { BlockchainElectrum } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
// Instantiate and start the Electrum Blockchain Adapter.
const blockchain = new BlockchainElectrum();
await blockchain.start();
// Fetch a transaction.
const tx = await blockchain.fetchTransaction('a23bbd321067fd039da53f007b26f62591e58e6554b0488d83534c032dbca38b');
// Log it to the console.
console.log('Transaction Hex', tx.toHex());
```
</Playground>
+81
View File
@@ -0,0 +1,81 @@
---
aside: false
---
<script lang="ts" setup>
import CodeSandbox from '@theme/components/CodeSandbox.vue'
</script>
# Sandbox
We can include Code Sandboxes in our documentation.
This'll be very useful for:
1. Guides
2. Testing
## Primitives
The below is Address mess.
<!-- NOTE: It is absolutely crucial you leave a blank line before and after the backticks! Otherwise, you'll end up with errors.-->
<CodeSandbox>
```
const { Address } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
const lockscriptHex = Address.from('bitcoincash:qr2084lauctzarqk20kqlhef8gyneqhceqy75wvw8q').toLockscriptHex();
console.log(lockscriptHex);
// Testing one new line
console.log({
someText: 'abc'
});
console.info('yep');
```
</CodeSandbox>
## Blockchain Connections
We can have more than one per page too. For example:
<!-- NOTE: It is absolutely crucial you leave a blank line before and after the backticks! Otherwise, you'll end up with errors.-->
<CodeSandbox>
```
const { BlockchainElectrum } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
const blockchain = new BlockchainElectrum();
await blockchain.start();
const tx = await blockchain.fetchTransaction('a23bbd321067fd039da53f007b26f62591e58e6554b0488d83534c032dbca38b');
console.log('Transaction Hex', tx.toHex());
```
</CodeSandbox>
## Wallet Example
The below demonstrates instantiating a wallet.
<!-- NOTE: It is absolutely crucial you leave a blank line before and after the backticks! Otherwise, you'll end up with errors.-->
<CodeSandbox>
```
const { BlockchainElectrum, WalletP2PKH } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
const wallet = WalletP2PKH.generateRandom({
blockchain: new BlockchainElectrum
});
const address = await wallet.getReceivingAddress();
console.log(address.toCashAddr());
```
</CodeSandbox>