Merge branch 'add_codesandbox' into 'development'
Add codesandbox See merge request GeneralProtocols/xo/stack!51
This commit is contained in:
Generated
+29
-25
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,7 +9,5 @@ import './tools.css';
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
},
|
||||
enhanceApp({ _app, _router, _siteData }) {},
|
||||
} satisfies Theme;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user