Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b4132b5
fix for empty layers toggle showing small circle
cycle4passion Jan 11, 2026
56945c5
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 11, 2026
3aaa686
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 14, 2026
07b33de
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 16, 2026
26c0b12
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 19, 2026
e5b8d25
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 19, 2026
617112a
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 21, 2026
20f4b33
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 24, 2026
60ca72b
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 25, 2026
077da76
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 25, 2026
3268020
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 25, 2026
b1057d2
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 26, 2026
20e5cf5
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 26, 2026
ad1d5c1
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 27, 2026
555775e
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 31, 2026
9c170a6
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Jan 31, 2026
658f85e
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Feb 1, 2026
f8e3a88
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Feb 1, 2026
1bd9609
Merge remote-tracking branch 'upstream/docs-v2' into docs-v2
cycle4passion Feb 1, 2026
7d10157
svelte-repl
cycle4passion Feb 6, 2026
d8fa441
Added Command-Shift-S hidden hotkey to save +page.svelte to location …
cycle4passion Feb 6, 2026
4762444
changed name of cmd-shift-s hotkey saved component to new-example.svelte
cycle4passion Feb 6, 2026
63b1a7c
- add wrapping div option
cycle4passion Feb 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ declare global {
// interface PageState {}
// interface Platform {}
}

// File System Access API (not in default DOM lib)
interface FilePickerAcceptType {
description?: string;
accept?: Record<string, string | string[]>;
}
interface SaveFilePickerOptions {
suggestedName?: string;
types?: FilePickerAcceptType[];
excludeAcceptAllOption?: boolean;
}
interface Window {
showSaveFilePicker(options?: SaveFilePickerOptions): Promise<FileSystemFileHandle>;
}
}

export {};
47 changes: 47 additions & 0 deletions docs/src/lib/components/EditWithButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import { Button, Toggle, ButtonGroup, Icon, MenuItem, Menu } from 'svelte-ux';
import { openInSvelteREPL } from '$lib/utils/svelte-repl';
import { openInStackBlitz } from '$lib/utils/stackblitz.svelte';

import ChevronDownIcon from '~icons/lucide/chevron-down';
import LucideFilePen from '~icons/lucide/file-pen';
import StackBlitzIcon from '~icons/simple-icons/stackblitz';
import SvelteIcon from '~icons/simple-icons/svelte';

let { component, source, name } = $props();

let isOpen = $state(false);
</script>

<ButtonGroup class="text-surface-content/70 py-1">
<Toggle bind:on={isOpen} let:on={open} let:toggle let:toggleOff>
<Button icon={LucideFilePen} on:click={toggle} class="py-1">
Edit in
<span style="transition: transform 300ms ease; transform: rotate({open ? -180 : 0}deg);">
<ChevronDownIcon />
</span>
<Menu {open} on:close={toggleOff} placement="bottom-start" class="z-25">
<MenuItem
class="text-surface-content/70 hover:text-surface-content"
onclick={() => {
toggleOff();
openInSvelteREPL(source);
}}
>
<Icon data={SvelteIcon} />
Svelte REPL (beta)
</MenuItem>
<MenuItem
class="text-surface-content/70 hover:text-surface-content"
onclick={() => {
toggleOff();
openInStackBlitz(component, name);
}}
>
<Icon data={StackBlitzIcon} />
StackBlitz
</MenuItem>
</Menu>
</Button>
</Toggle>
</ButtonGroup>
11 changes: 2 additions & 9 deletions docs/src/lib/components/Example.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { SvelteComponent } from 'svelte';
import { slide } from 'svelte/transition';
import { Button, CopyButton, Dialog, Toggle, Tooltip } from 'svelte-ux';
import EditWithButton from './EditWithButton.svelte';
import { cls } from '@layerstack/tailwind';

import { examples } from '$lib/context';
Expand All @@ -11,11 +12,9 @@
import LucideCode from '~icons/lucide/code';
import LucideFullscreen from '~icons/lucide/fullscreen';
import LucideTable from '~icons/lucide/table';
import LucideFilePen from '~icons/lucide/file-pen';
import LucideGripVertical from '~icons/lucide/grip-vertical';

import { page } from '$app/state';
import { openInStackBlitz } from '$lib/utils/stackblitz.svelte';
import { movable } from '$lib/actions/movable';

let {
Expand Down Expand Up @@ -240,13 +239,7 @@
{/if}

{#if component && name}
<Button
icon={LucideFilePen}
class="text-surface-content/70 py-1"
on:click={() => openInStackBlitz(component, name)}
>
Edit
</Button>
<EditWithButton {component} source={example.source} {name} />
{/if}
</div>
{/if}
Expand Down
228 changes: 228 additions & 0 deletions docs/src/lib/utils/svelte-repl.ts
Original file line number Diff line number Diff line change
@@ -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<string, () => Promise<string>>;

async function readFile(path: string): Promise<string> {
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<File[]> {
// 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(/<script[^>]*>[\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<div${attrs ? ` ${attrs}` : ''}>\n\t${markup}\n</div>\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<p class="!hidden" style="display:flex;justify-content:space-between;background:red;padding:4px;font-size:16px;text-align:center;"><span>↖</span><span>Please toggle on Tailwind setting to see the LayerChart (<a href="https://github.com/sveltejs/svelte.dev/issues/1220" target="_blank" rel="noopener noreferrer">Issue #1220</a>)</span><span></span></p>`
);
}

// Add theme CSS
contents += `\n<style>
:global(.lc-root-container) {
color-scheme: light;
--color-primary: hsl(217, 91%, 60%);
--color-primary-content: hsl(0, 0%, 100%);
--color-secondary: hsl(25, 95%, 53%);
--color-surface-100: hsl(0, 0%, 100%);
--color-surface-200: hsl(220, 14%, 96%);
--color-surface-300: hsl(216, 12%, 84%);
--color-surface-content: hsl(221, 39%, 11%);
}
@media (prefers-color-scheme: dark) {
:global(.lc-root-container) {
color-scheme: dark;
--color-primary: hsl(217, 91%, 60%);
--color-primary-content: hsl(0, 0%, 100%);
--color-secondary: hsl(25, 95%, 53%);
--color-surface-100: hsl(240, 4%, 16%);
--color-surface-200: hsl(240, 6%, 10%);
--color-surface-300: hsl(240, 10%, 4%);
--color-surface-content: hsl(240, 5%, 96%);
}
}
:global(.lc-root-container) {
--color-apples: hsl(142, 71%, 45%);
--color-bananas: hsl(48, 96%, 53%);
--color-cherries: hsl(0, 84%, 60%);
--color-grapes: hsl(271, 91%, 65%);
--color-oranges: hsl(25, 95%, 53%);
}
</style>
`;
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}`;
}
Loading
Loading