diff --git a/packages/web/package.json b/packages/web/package.json index 2ec8746..d45de23 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,6 +18,7 @@ "@pandacss/shared": "^0.7.0", "@xstate/react": "^3.2.2", "esbuild-wasm": "^0.18.14", + "lz-string": "^1.5.0", "monaco-editor": "0.40.0", "pastable": "^2.2.0", "react": "^18.2.0", diff --git a/packages/web/src/Playground/Playground.machine.ts b/packages/web/src/Playground/Playground.machine.ts index 9202bda..11400af 100644 --- a/packages/web/src/Playground/Playground.machine.ts +++ b/packages/web/src/Playground/Playground.machine.ts @@ -8,6 +8,7 @@ import { initialInputList, initialOutputList } from "../../../../demo-code-sampl import { createMergeCss } from "@pandacss/shared"; import { createPandaContext, createTailwindContext, rewriteTwFileContentToPanda, type TwResultItem } from "tw2panda"; +import { UrlSaver } from "./url-saver"; type PlaygroundContext = { monaco: Monaco | null; @@ -33,13 +34,16 @@ type PlaygroundEvent = | { type: "Select output tab"; name: string } | { type: "Update input"; value: string }; +const urlSaver = new UrlSaver(); +const initialAppInput = urlSaver.getValue("input") || initialInputList["tw-App.tsx"]; + const initialContext: PlaygroundContext = { monaco: null, inputEditor: null, outputEditor: null, sourceFile: null, resultList: [], - inputList: initialInputList, + inputList: { ...initialInputList, "tw-App.tsx": initialAppInput }, selectedInput: "tw-App.tsx", outputList: initialOutputList, selectedOutput: "App.tsx", @@ -115,10 +119,13 @@ export const playgroundMachine = createMachine( } return { ...ctx, inputList }; }), + updateUrl(context) { + urlSaver.setValue("input", context.inputList[context.selectedInput] ?? ""); + }, updateInput: choose([ { cond: "isAppFile", - actions: ["updateSelectedInput", "extractClassList"], + actions: ["updateSelectedInput", "updateUrl", "extractClassList"], }, { actions: ["updateSelectedInput"] }, ]), diff --git a/packages/web/src/Playground/Playground.machine.typegen.ts b/packages/web/src/Playground/Playground.machine.typegen.ts index 6f4da32..52240e1 100644 --- a/packages/web/src/Playground/Playground.machine.typegen.ts +++ b/packages/web/src/Playground/Playground.machine.typegen.ts @@ -19,6 +19,7 @@ export interface Typegen0 { selectOutputTab: "Select output tab"; updateInput: "Editor Loaded" | "Select input tab" | "Update input"; updateSelectedInput: "Editor Loaded" | "Select input tab" | "Update input"; + updateUrl: "Editor Loaded" | "Select input tab" | "Update input"; }; eventsCausingDelays: {}; eventsCausingGuards: { diff --git a/packages/web/src/Playground/url-saver.ts b/packages/web/src/Playground/url-saver.ts new file mode 100644 index 0000000..0e93f39 --- /dev/null +++ b/packages/web/src/Playground/url-saver.ts @@ -0,0 +1,67 @@ +import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string' + +// adapted from https://github.dev/dsherret/ts-ast-viewer/blob/c71e238123d972bae889b3829e23b44f39d8d5c2/site/src/utils/UrlSaver.ts#L1-L29 +function getDecompressedStringFromUrl(name: string) { + if (typeof window === 'undefined') return + + const search = new URLSearchParams(window.location.search) + const code = (search.get(name) ?? '').trim() + return decompressFromEncodedURIComponent(code) ?? '' // will be null on error +} + +function updateUrlWithCompressedString(name: string, value: string) { + if (value.length === 0) { + updateUrlWithParam(name, '') + } else { + const compressed = compressToEncodedURIComponent(value) + const url = new URL(window.location.href) + url.searchParams.set(name, compressed) + + // completely arbitrary limit of characters, but it appears to not work anymore around that + if (url.toString().length >= 14_500) { + throw new Error('The compressed string is too large to be stored in the URL.') + } else { + updateUrlWithParam(name, compressed) + } + } +} + +function updateUrlWithParam(name: string, value: string | number) { + if (typeof window === 'undefined') return + + const url = new URL(window.location.href) + url.searchParams.set(name, String(value)) + window.history.replaceState(undefined, '', url) +} + +const resetUrl = () => { + if (typeof window === 'undefined') return + + window.history.replaceState(undefined, '', window.location.origin + window.location.pathname) +} + +const deletingParamInUrl = (name: string) => { + if (typeof window === 'undefined') return + + const url = new URL(window.location.href) + url.searchParams.delete(name) + window.history.replaceState(undefined, '', url) +} + +export class UrlSaver { + getValue(name: string) { + return getDecompressedStringFromUrl(name) + } + + setValue(name: string, value: string) { + updateUrlWithCompressedString(name, value) + } + + reset(name: string) { + deletingParamInUrl(name) + } + + resetAll() { + resetUrl() + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80822ac..bf85c2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ importers: esbuild-wasm: specifier: ^0.18.14 version: 0.18.14 + lz-string: + specifier: ^1.5.0 + version: 1.5.0 monaco-editor: specifier: 0.40.0 version: 0.40.0 @@ -6599,6 +6602,11 @@ packages: engines: {node: '>=12'} dev: true + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: false + /magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'}