diff --git a/docs/src/lib/components/EditWithButton.svelte b/docs/src/lib/components/EditWithButton.svelte new file mode 100644 index 000000000..d86c4683e --- /dev/null +++ b/docs/src/lib/components/EditWithButton.svelte @@ -0,0 +1,47 @@ + + + + + + + diff --git a/docs/src/lib/components/Example.svelte b/docs/src/lib/components/Example.svelte index f146bb7ed..59fd78645 100644 --- a/docs/src/lib/components/Example.svelte +++ b/docs/src/lib/components/Example.svelte @@ -1,16 +1,8 @@
@@ -209,11 +153,7 @@ style:width={containerWidth ? `${containerWidth}px` : undefined} style:view-transition-name={viewTransitionName} > - {#if isVisible} - - {:else} -
- {/if} + {#if canResize}
openInStackBlitz(component, name)} - > - Edit - + {/if} - - - -
{/if} {:else} @@ -338,14 +253,3 @@
{/if} - -{#if svgUnavailable} -
- (svgUnavailable = false)} - /> -
-{/if} diff --git a/docs/src/lib/utils/svelte-repl.ts b/docs/src/lib/utils/svelte-repl.ts new file mode 100644 index 000000000..76c06b761 --- /dev/null +++ b/docs/src/lib/utils/svelte-repl.ts @@ -0,0 +1,228 @@ +interface File { + name: string; + source: string; +} + +// Lazy-load $lib source files for REPL import resolution (loaded on demand, not eagerly) +const libModules = import.meta.glob(['/src/lib/**/*.{ts,js,svelte}'], { + query: '?raw', + import: 'default' +}) as Record Promise>; + +async function readFile(path: string): Promise { + const key = `/${path}`; + const loader = libModules[key]; + if (!loader) throw new Error(`File not found: ${path}`); + return loader(); +} + +/** + * Parse local imports from a source file, resolve them using the readFile function, + * and return an array of files ready for the Svelte playground (with App.svelte as entry point). + */ +export async function accumulateReplFiles(source: string): Promise { + // Parse relative, $lib, and absolute path imports + const importPattern = + /import\s+(?:\{[^}]+\}|\w+)\s+from\s+['"]((?:\.|\.\.|\$lib|\/)\/[^'"]+)['"]/g; + const localImports: { importPath: string; fileName: string; resolvedPath: string }[] = []; + let match; + while ((match = importPattern.exec(source)) !== null) { + const importPath = match[1]; + const fileName = importPath.split('/').pop() ?? importPath; + + let resolvedPath: string; + if (importPath.startsWith('$lib/')) { + resolvedPath = `src/lib/${importPath.slice('$lib/'.length)}`; + } else { + resolvedPath = `src/routes/${importPath.replace(/^\.\//, '')}`; + } + + localImports.push({ importPath, fileName, resolvedPath }); + } + + const files: File[] = []; + let rewrittenMain = source; + + for (const { importPath, fileName, resolvedPath } of localImports) { + // First pass: rewrite import to flat filename + rewrittenMain = rewrittenMain.replace( + new RegExp(`from\\s+['"]${importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`), + `from './${fileName}'` + ); + + let actualPath = resolvedPath; + try { + let fileSource: string; + try { + fileSource = await readFile(resolvedPath); + } catch { + const hasExtension = /\.\w+$/.test(resolvedPath); + if (hasExtension && resolvedPath.endsWith('.js')) { + actualPath = resolvedPath.replace(/\.js$/, '.ts'); + fileSource = await readFile(actualPath); + } else if (!hasExtension) { + // Try common extensions for extensionless imports + let resolved: string | undefined; + for (const ext of ['.ts', '.js', '.svelte']) { + try { + actualPath = resolvedPath + ext; + fileSource = await readFile(actualPath); + break; + } catch { + /* try next extension */ + } + } + if (!fileSource!) throw new Error(`File not found: ${resolvedPath}`); + } else { + throw new Error(`File not found: ${resolvedPath}`); + } + } + const actualFileName = actualPath.split('/').pop() ?? actualPath; + files.push({ name: actualFileName, source: fileSource }); + + // Rewrite import to use actual filename if extension changed + if (actualFileName !== fileName) { + rewrittenMain = rewrittenMain.replace(`from './${fileName}'`, `from './${actualFileName}'`); + } + } catch { + console.warn(`Could not read import: ${resolvedPath}`); + } + } + + // App.svelte must be first (it's the entry point) + files.unshift({ name: 'App.svelte', source: rewrittenMain }); + + return files; +} + +export async function openInSvelteREPL(source: string) { + const files = await accumulateReplFiles(source); + const url = await createSvelteReplUrl(files); + window.open(url, '_blank'); +} + +export async function createSvelteReplUrl(files: File[]) { + const useTailWind = false; + // Temporary: use layerchart@next until layerchart is published as latest + const useNext = true; + // Temporary: use container until layerchart-docs2 is published to latest + const useContainer = true; + // Temporary add tailwind notice to the beginning of markup + const addTailwindNotice = false; + + const playgroundFiles = files.map((f) => { + let contents = f.source; + if (f.name === 'App.svelte') { + const scriptsection = contents.match(/]*>[\s\S]*?<\/script>/)?.[0]; + if (scriptsection) contents = contents.replace(scriptsection, ''); + if (useContainer) { + // add container div with possible height and width attributes + const componentOne = contents.match(/<[A-Z]\w*\b[\s\S]*?>/)?.[0]; + const height = componentOne?.match(/height=\{(\d+)\}/)?.[1]; + const width = componentOne?.match(/width=\{(\d+)\}/)?.[1]; + const attrs = [height && `style:height="${height}px"`, width && `style:width="${width}px"`] + .filter(Boolean) + .join(' '); + const markup = contents.trim().split('\n').join('\n\t'); + contents = `${scriptsection ?? ''}\n\n\t${markup}\n\n`; + } + + if (useNext) { + contents = contents.replace(/from\s+['"]layerchart['"]/g, "from 'layerchart@next'"); + } + + if (useTailWind && addTailwindNotice) { + // Add conditionally shown tailwind notice to the beginning of markup + contents = contents.replace( + /(<\/script>)/, + `$1\n\n

Please toggle on Tailwind setting to see the LayerChart (Issue #1220)

` + ); + } + + // Add theme CSS + contents += `\n +`; + contents = contents.trim(); + } + + return { + type: 'file', + name: f.name, + basename: f.name, + contents, + text: true + }; + }); + const data = { tailwind: useTailWind, files: playgroundFiles }; + const json = JSON.stringify(data); + + // Convert string to Uint8Array + const encoder = new TextEncoder(); + const uint8Array = encoder.encode(json); + + // Compress using CompressionStream (modern browsers) + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(uint8Array); + controller.close(); + } + }); + + const compressedStream = stream.pipeThrough(new CompressionStream('gzip')); + + // Read the compressed data + const reader = compressedStream.getReader(); + const chunks = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + + // Combine chunks into single Uint8Array + const compressed = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); + let offset = 0; + for (const chunk of chunks) { + compressed.set(chunk, offset); + offset += chunk.length; + } + + // Base64 encode (URL-safe) + const base64 = btoa(String.fromCharCode(...compressed)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + + return `https://svelte.dev/playground#${base64}`; +} diff --git a/docs/src/routes/docs/playground/+page.svelte b/docs/src/routes/docs/playground/+page.svelte index 6377bf743..213ff76ea 100644 --- a/docs/src/routes/docs/playground/+page.svelte +++ b/docs/src/routes/docs/playground/+page.svelte @@ -32,19 +32,7 @@ import CodeEditor from './CodeEditor.svelte'; import { Overlay, ProgressCircle } from 'svelte-ux'; import { AnsiUp } from 'ansi_up'; - - /* TODO: - - incase you didn't see it you can click console to expand/collapse. - - second pass background colors, etc. I'm always open for revisions. - - console output seems to missing some later stage loading state - "load preview..." - - filter blank \n going to console output? - - collapse console when loaded is working, but performing too early. - - Refresh correct, or something thru webcontainer? - - Buttons from Svelte-UX w/Icons - icons not centered vertically. - - Code - Open Project in StackBlitz - - Code - Open Project in Svelte REPL - - Code - Download Project - */ + import { openInSvelteREPL } from '$lib/utils/svelte-repl'; let { data }: { data: PageData } = $props(); const ansiUp = new AnsiUp(); @@ -188,7 +176,14 @@ // Open Project in REPL async function openInREPL() { - // This needs wired up + if (!webcontainerInstance) return; + + try { + const mainSource = await webcontainerInstance.fs.readFile('src/routes/+page.svelte', 'utf-8'); + await openInSvelteREPL(mainSource); + } catch (err) { + console.error('Failed to open in REPL:', err); + } } onMount(async () => { @@ -343,6 +338,40 @@ } // Handle click outside of file tree to close it + // Save current +page.svelte to local filesystem via system save dialog + async function saveNewExample() { + if (!webcontainerInstance) return; + + try { + const content = await webcontainerInstance.fs.readFile('src/routes/+page.svelte', 'utf-8'); + + const handle = await window.showSaveFilePicker({ + suggestedName: 'new-example.svelte', + types: [ + { + description: 'Svelte files', + accept: { 'text/plain': ['.svelte'] } + } + ] + }); + + const writable = await handle.createWritable(); + await writable.write(content); + await writable.close(); + } catch (err: any) { + // User cancelled the dialog + if (err.name === 'AbortError') return; + console.error('Failed to save example:', err); + } + } + + function handleKeydown(event: KeyboardEvent) { + if (event.metaKey && event.shiftKey && event.key === 's') { + event.preventDefault(); + saveNewExample(); + } + } + function handleClickOutside(event: MouseEvent) { // Don't close if clicking on SVG icons (folder toggles) const target = event.target as Element; @@ -354,7 +383,7 @@ } - +
@@ -400,23 +429,14 @@