Merge branch '81-feat-playground-intermittent-save' into 'development'

Resolve "feat(Playground): Intermittent Save"

Closes #81

See merge request GeneralProtocols/xo/stack!122
This commit is contained in:
Jim Hamill
2025-08-12 05:29:34 +00:00
3 changed files with 131 additions and 32 deletions
+4
View File
@@ -259,5 +259,9 @@ export default withMermaid({
target: 'esnext',
},
},
resolve: {
// Configure to use browser version of @xocash/stack
conditions: ['browser', 'import', 'module', 'default'],
},
},
});
@@ -2,7 +2,7 @@
<div class="code-sandbox" :style="sandboxStyle">
<!-- Editor Container -->
<div class="editor-container" :style="editorContainerStyle">
<code-editor ref="editorRef" v-model="code" @execute="executeCode" :style="editorStyle" :libs="libs" :is-dark="isDark"/>
<code-editor ref="editorRef" v-model="code" @update:model-value="saveSandbox" @execute="executeCode" :style="editorStyle" :libs="libs" :is-dark="isDark"/>
<div class="editor-buttons">
<button v-if="!hideRunButton" @click="executeCode" class="primary-button ">Run Code</button>
</div>
@@ -51,6 +51,8 @@ import iframeTemplateHtml from './CodeSandboxTemplate.html?raw';
import CodeEditor from './CodeEditor.vue';
import { ImportedLib, useXOCashLibs } from '../utils/useXOCashLibs';
import { StorageLocalStorage, BaseStore } from '@xocash/stack';
const { isDark } = useData();
const xocashLibs = useXOCashLibs();
@@ -62,6 +64,7 @@ export type CodeSandboxProps = {
outputHeight?: string;
hideRunButton?: boolean;
libs?: Record<string, ImportedLib>;
saveable?: boolean;
}
// Define props for the component
@@ -73,6 +76,7 @@ const props = withDefaults(defineProps<CodeSandboxProps>(), {
outputHeight: 'auto',
hideRunButton: false,
libs: (): Record<string, ImportedLib> => ({}),
saveable: false,
});
// Define types for console logs
@@ -97,6 +101,10 @@ const showLogs = ref(false);
const sandboxIframe = ref<HTMLIFrameElement | null>(null);
const editorRef = ref<InstanceType<typeof CodeEditor> | null>(null);
const sandboxStorage = ref<StorageLocalStorage | null>(null);
const sandboxStore = ref<BaseStore | null>(null);
let saveTimeout: NodeJS.Timeout | null = null;
// Computed styles based on props
const sandboxStyle = computed(() => ({
height: props.fullHeight ? '100%' : props.height,
@@ -313,25 +321,26 @@ const handleMessage = (event: MessageEvent) => {
};
// 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='
onMounted(async () => {
sandboxStorage.value = await StorageLocalStorage.createOrOpen('xoCodeSandbox_V0.0.1');
sandboxStore.value = await sandboxStorage.value.createOrGetStore('codeSandbox');
// 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();
const fromCodeParam = (): string | undefined => {
if (window.location.hash && window.location.hash.startsWith('#code=')) {
const codeParam = window.location.hash.substring(6); // Remove '#code='
try {
// Always assume Base64 encoding
return 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
});
}
}
} else {
// Otherwise check query parameters (for backward compatibility)
const urlParams = new URLSearchParams(window.location.search);
const queryCodeParam = urlParams.get('code');
@@ -339,19 +348,68 @@ onMounted(() => {
if (queryCodeParam) {
try {
// Try to decode from Base64
code.value = decodeURIComponent(escape(atob(decodeURIComponent(queryCodeParam))));
return 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();
}
return undefined;
}
const fromCodeSlot = (): string | undefined => {
// Try to read the code from the code slot
if (codeRef.value) {
return codeRef.value.innerText.trim();
}
return undefined;
}
const fromSessionStorage = (): string | undefined => {
if (!props.saveable) {
return undefined;
}
// Try to read the code from session storage
const sessionStorageKey = 'xoCodeSandbox_V0.0.1';
const sessionStorage = window.sessionStorage.getItem(sessionStorageKey);
if (!sessionStorage) {
return undefined;
}
// Parse the session storage data
const sessionStorageData = JSON.parse(sessionStorage);
if (sessionStorageData) {
return sessionStorageData.code;
}
return undefined;
}
const fromLocalStorage = async (): Promise<string | undefined> => {
if (!props.saveable || !sandboxStore.value) {
return undefined;
}
// Try to read the code from local storage and update the sandbox code
const sandboxTemp = await sandboxStore.value.get<{ code: string }>('sandbox_temp');
if (sandboxTemp) {
return sandboxTemp.code;
}
return undefined;
}
// Try to load the code from the various sources
// 1. URL hash
// 2. Saveable ? Session storage
// 3. Saveable ? Local storage
// 4. Code slot
const importedCode = await Promise.all([fromCodeParam(), fromSessionStorage(), fromLocalStorage(), fromCodeSlot()]);
const codeToLoad = importedCode.find(code => code !== undefined);
code.value = codeToLoad || '';
// Get the selected version from the url if it exists
const urlParams = new URLSearchParams(window.location.search);
const versionParam = urlParams.get('version');
@@ -364,19 +422,55 @@ onMounted(() => {
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();
}
/**
* Saves the sandbox code to both local and session storage
* This function is debounced to avoid excessive saves during typing
*/
const saveSandbox = async () => {
if (!props.saveable) {
return;
}
};
// Clear any existing timeout to debounce the save operation
if (saveTimeout) {
clearTimeout(saveTimeout);
}
// Define our different methods of saving the sandbox
const toLocalStorage = async () => {
// Save the code to the local storage
if (!sandboxStore.value) {
return;
}
await sandboxStore.value.set('sandbox_temp', {
code: code.value
});
}
const toSessionStorage = async () => {
const sessionStorageKey = 'xoCodeSandbox_V0.0.1';
window.sessionStorage.setItem(sessionStorageKey, JSON.stringify({
code: code.value,
}));
}
// Set a new timeout to save after 500ms of inactivity
saveTimeout = setTimeout(async () => {
await Promise.all([toLocalStorage(), toSessionStorage()]);
saveTimeout = null;
}, 500);
}
onBeforeUnmount(() => {
// Clean up event listener
window.removeEventListener('message', handleMessage);
// Clean up any pending save timeout
if (saveTimeout) {
clearTimeout(saveTimeout);
saveTimeout = null;
}
});
// Expose our functions so that they can be called from parent components.
@@ -23,6 +23,7 @@
hideRunButton
:libs="xocashLibs"
:isDark="isDark"
:saveable="true"
>
<slot></slot>
</CodeSandbox>