From c53cc1384f8ebe72bf3e196fc4781bc959a2d9f6 Mon Sep 17 00:00:00 2001 From: Harvey Zuccon Date: Tue, 12 Aug 2025 05:29:34 +0000 Subject: [PATCH] Resolve "feat(Playground): Intermittent Save" --- packages/docs/src/.vitepress/config.ts | 4 + .../theme/components/CodeSandbox.vue | 158 ++++++++++++++---- .../theme/components/Playground.vue | 1 + 3 files changed, 131 insertions(+), 32 deletions(-) diff --git a/packages/docs/src/.vitepress/config.ts b/packages/docs/src/.vitepress/config.ts index 14bf0dc..5cb5a60 100644 --- a/packages/docs/src/.vitepress/config.ts +++ b/packages/docs/src/.vitepress/config.ts @@ -259,5 +259,9 @@ export default withMermaid({ target: 'esnext', }, }, + resolve: { + // Configure to use browser version of @xocash/stack + conditions: ['browser', 'import', 'module', 'default'], + }, }, }); diff --git a/packages/docs/src/.vitepress/theme/components/CodeSandbox.vue b/packages/docs/src/.vitepress/theme/components/CodeSandbox.vue index 60fc047..2fe7027 100644 --- a/packages/docs/src/.vitepress/theme/components/CodeSandbox.vue +++ b/packages/docs/src/.vitepress/theme/components/CodeSandbox.vue @@ -2,7 +2,7 @@
- +
@@ -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; + saveable?: boolean; } // Define props for the component @@ -73,6 +76,7 @@ const props = withDefaults(defineProps(), { outputHeight: 'auto', hideRunButton: false, libs: (): Record => ({}), + saveable: false, }); // Define types for console logs @@ -97,6 +101,10 @@ const showLogs = ref(false); const sandboxIframe = ref(null); const editorRef = ref | null>(null); +const sandboxStorage = ref(null); +const sandboxStore = ref(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 => { + 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. diff --git a/packages/docs/src/.vitepress/theme/components/Playground.vue b/packages/docs/src/.vitepress/theme/components/Playground.vue index d7661ee..0d48606 100644 --- a/packages/docs/src/.vitepress/theme/components/Playground.vue +++ b/packages/docs/src/.vitepress/theme/components/Playground.vue @@ -23,6 +23,7 @@ hideRunButton :libs="xocashLibs" :isDark="isDark" + :saveable="true" >