Full Code of solidjs/solid-playground for AI

main 4d6a22c5a338 cached
63 files
142.8 KB
37.5k tokens
64 symbols
1 requests
Download .txt
Repository: solidjs/solid-playground
Branch: main
Commit: 4d6a22c5a338
Files: 63
Total size: 142.8 KB

Directory structure:
gitextract_mlfkxwgk/

├── .gitignore
├── .oxfmtrc.json
├── .oxlintrc.json
├── LICENSE
├── README.md
├── package.json
├── packages/
│   ├── playground/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public/
│   │   │   ├── _redirects
│   │   │   ├── manifest.webmanifest
│   │   │   ├── robots.txt
│   │   │   └── sw.js
│   │   ├── src/
│   │   │   ├── app.tsx
│   │   │   ├── components/
│   │   │   │   ├── header.tsx
│   │   │   │   ├── setupSolid.ts
│   │   │   │   ├── update.tsx
│   │   │   │   └── zoomDropdown.tsx
│   │   │   ├── context.tsx
│   │   │   ├── index.tsx
│   │   │   ├── pages/
│   │   │   │   ├── edit.tsx
│   │   │   │   ├── home.tsx
│   │   │   │   └── login.tsx
│   │   │   └── utils/
│   │   │       ├── date.ts
│   │   │       ├── exportFiles.tsx
│   │   │       ├── isDarkTheme.ts
│   │   │       └── serviceWorker.ts
│   │   ├── tsconfig.json
│   │   ├── unocss.config.ts
│   │   └── vite.config.ts
│   └── solid-repl/
│       ├── build.ts
│       ├── package.json
│       ├── repl/
│       │   ├── compiler.ts
│       │   ├── formatter.ts
│       │   ├── linter.ts
│       │   └── main.css
│       ├── src/
│       │   ├── components/
│       │   │   ├── CompileMode.tsx
│       │   │   ├── editor/
│       │   │   │   ├── index.tsx
│       │   │   │   ├── monacoTabs.tsx
│       │   │   │   └── setupSolid.ts
│       │   │   ├── error.tsx
│       │   │   ├── newTab.tsx
│       │   │   ├── preview.tsx
│       │   │   ├── repl.tsx
│       │   │   └── ui/
│       │   │       ├── Button.tsx
│       │   │       ├── Checkbox.tsx
│       │   │       ├── IconButton.tsx
│       │   │       ├── Input.tsx
│       │   │       ├── Label.tsx
│       │   │       └── Menu.tsx
│       │   ├── dockview/
│       │   │   └── solid.tsx
│       │   ├── hooks/
│       │   │   └── useZoom.ts
│       │   ├── index.ts
│       │   ├── repl.tsx
│       │   └── types.d.ts
│       ├── tsconfig.build.json
│       ├── tsconfig.json
│       └── unocss.config.ts
├── patches/
│   └── monaco-editor.patch
├── pnpm-workspace.yaml
├── scripts/
│   ├── oxlint-plugin-unocss.ts
│   └── unocss-worker.ts
├── tsconfig.json
└── uno.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules/
dist/
.DS_Store


================================================
FILE: .oxfmtrc.json
================================================
{
  "$schema": "./node_modules/oxfmt/configuration_schema.json",
  "arrowParens": "always",
  "htmlWhitespaceSensitivity": "ignore",
  "printWidth": 120,
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "useTabs": false,
  "quoteProps": "consistent",
  "ignorePatterns": ["node_modules/", "dist/", "pnpm-lock.yaml"],
  "sortTailwindcss": {
    "config": "uno.config.js"
  }
}


================================================
FILE: .oxlintrc.json
================================================
{
  "jsPlugins": ["./scripts/oxlint-plugin-unocss.ts"],
  "rules": {
    "unocss/valid-class": "error",
    "unocss/no-conflicting-classes": "error",
    "eslint/no-unassigned-vars": "off"
  }
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 SolidJS Core Team

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p>
  <img width="100%" src="https://assets.solidjs.com/banner?project=Playground&type=core" alt="Solid Playground">
</p>

# Solid Template Explorer

This is the source code of the [solid playground](https://playground.solidjs.com) website.
Through it you can quickly discover what the solid compiler will generate from your JSX templates.

There are 3 modes available:

- DOM: The classic SPA generation mechanism
- SSR: The server side generation mechanism
- HYDRATION: The client side generation for hydration
- UNIVERSAL: The client side generation for universal (custom renderer)

## Getting up and running

This project is built using the [pnpm](https://pnpm.js.org/) package manager.

Once you got it up and running you can follow these steps the have a fully working environement:

```bash
# Clone the project
$ git clone https://github.com/solidjs/solid-playground

# cd into the project and install the dependencies
$ cd solid-playground && pnpm i

# Start the dev server, the address is available at http://localhost:5173
$ pnpm run dev

# Build the project
$ pnpm run build
```

## Credits / Technologies used

- [solid-js](https://github.com/solidjs/solid/): The view library
- [@babel/standalone](https://babeljs.io/docs/en/babel-standalone): The in-browser compiler. Solid compiler relies on babel
- [monaco](https://microsoft.github.io/monaco-editor/): The in-browser code editor. This is the code editor that powers VS Code
- [Windi CSS](https://windicss.org/): The CSS framework
- [vite](https://vitejs.dev/): The module bundler
- [workbox](https://developers.google.com/web/tools/workbox): The service worker generator
- [pnpm](https://pnpm.js.org/): The package manager
- [lz-string](https://github.com/pieroxy/lz-string): The string compression algorithm used to share REPL


================================================
FILE: package.json
================================================
{
  "name": "solid-playground-restructured",
  "version": "1.0.0",
  "private": true,
  "description": "",
  "keywords": [],
  "author": "",
  "workspaces": [
    "./packages/*"
  ],
  "type": "module",
  "scripts": {
    "start": "cd packages/playground && pnpm start",
    "build": "cd packages/playground && pnpm build",
    "dev": "cd packages/playground && pnpm dev",
    "format": "oxfmt .",
    "lint": "oxlint ."
  },
  "devDependencies": {
    "@changesets/cli": "2.31.0",
    "@oxlint/plugins": "^1.61.0",
    "@unocss/preset-wind3": "^66.6.8",
    "jiti": "^2.6.1",
    "oxfmt": "^0.46.0",
    "oxlint": "^1.61.0",
    "synckit": "^0.11.12",
    "unocss": "^66.6.8"
  },
  "packageManager": "pnpm@10.33.2",
  "pnpm": {
    "patchedDependencies": {
      "monaco-editor": "patches/monaco-editor.patch"
    }
  }
}


================================================
FILE: packages/playground/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="manifest" href="/manifest.webmanifest" />
    <link rel="shortcut icon" href="/logo.png" type="image/png" />
    <title>Solid Playground</title>
    <meta name="description" content="Quickly discover what the solid compiler will generate from your JSX template" />
  </head>
  <body>
    <div id="app"></div>
    <div id="update" class="bottom-10 left-10 z-12 fixed"></div>
    <script src="./src/index.tsx" type="module"></script>
  </body>
</html>


================================================
FILE: packages/playground/package.json
================================================
{
  "name": "solid-playground",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "vite build",
    "start": "vite preview",
    "dev": "vite",
    "tsc": "tsc"
  },
  "dependencies": {
    "@solid-primitives/scheduled": "^1.5.3",
    "@solidjs/router": "^0.16.1",
    "dedent": "^1.7.2",
    "solid-dismiss": "^1.8.2",
    "solid-heroicons": "^3.2.4",
    "solid-js": "1.9.12",
    "solid-repl": "workspace:*"
  },
  "devDependencies": {
    "@amoutonbrady/lz-string": "^0.1.0",
    "@babel/core": "^7.29.0",
    "@babel/plugin-syntax-jsx": "^7.28.6",
    "@babel/preset-typescript": "^7.28.5",
    "@babel/types": "^7.29.0",
    "@solidjs/router": "^0.16.1",
    "@types/babel__standalone": "^7.1.9",
    "@types/dedent": "^0.7.2",
    "assert": "^2.1.0",
    "csstype": "^3.2.3",
    "jszip": "^3.10.1",
    "monaco-editor": "^0.55.1",
    "register-service-worker": "^1.7.2",
    "typescript": "^6.0.3",
    "unocss": "^66.6.8",
    "vite": "^8.0.10",
    "vite-plugin-solid": "^2.11.12"
  }
}


================================================
FILE: packages/playground/public/_redirects
================================================
/*    /index.html   200


================================================
FILE: packages/playground/public/manifest.webmanifest
================================================
{
  "dir": "ltr",
  "lang": "en",
  "name": "Solid REPL",
  "short_name": "Solid REPL",
  "scope": "/",
  "display": "standalone",
  "start_url": "/",
  "background_color": "transparent",
  "theme_color": "transparent",
  "description": "Solid REPL",
  "orientation": "any",
  "prefer_related_applications": false,
  "icons": [
    {
      "src": "/square_logo.png",
      "sizes": "144x144",
      "type": "image/png"
    }
  ]
}


================================================
FILE: packages/playground/public/robots.txt
================================================
User-agent: *
Allow: /

================================================
FILE: packages/playground/public/sw.js
================================================
const cacheName = 'my-cache';

async function notifyClient(event) {
  const client = event.clientId ? await clients.get(event.clientId) : null;
  if (client) {
    client.postMessage({ type: 'cache' });
    return;
  }
  const all = await clients.matchAll();
  for (const c of all) c.postMessage({ type: 'cache' });
}

function responsesDiffer(cached, fresh) {
  const cachedTag = cached.headers.get('etag') || cached.headers.get('last-modified');
  const freshTag = fresh.headers.get('etag') || fresh.headers.get('last-modified');
  if (cachedTag && freshTag) return cachedTag !== freshTag;
  return false;
}

async function fetchAndCache(cache, event) {
  try {
    const response = await fetch(event.request);
    if (response.ok) {
      await cache.put(event.request, response.clone());
    }
    return response;
  } catch (e) {
    console.error(e);
    if (event.request.mode === 'navigate') {
      return await cache.match('/index.html');
    }
    throw e;
  }
}

async function fetchWithCache(event) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(event.request);
  const fresh = fetchAndCache(cache, event);
  if (cached) {
    fresh.then((response) => {
      if (response && responsesDiffer(cached, response)) {
        notifyClient(event);
      }
    });
    return cached;
  }
  return fresh;
}

function handleFetch(event) {
  if (
    event.request.headers.get('cache-control') !== 'no-cache' &&
    event.request.method === 'GET' &&
    event.request.url.startsWith(self.location.origin)
  ) {
    event.respondWith(fetchWithCache(event));
  }
}

self.addEventListener('fetch', handleFetch);

self.addEventListener('install', () => {
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    (async () => {
      const keys = await caches.keys();
      await Promise.all(keys.filter((k) => k !== cacheName).map((k) => caches.delete(k)));
      await clients.claim();
    })(),
  );
});


================================================
FILE: packages/playground/src/app.tsx
================================================
import { Show, JSX, Suspense } from 'solid-js';
import { Route, Router } from '@solidjs/router';
import { eventBus, setEventBus } from './utils/serviceWorker';
import { Update } from './components/update';
import { useZoom } from 'solid-repl/src/hooks/useZoom';
import { Edit } from './pages/edit';
import { Home } from './pages/home';
import { Login } from './pages/login';
import { AppContextProvider } from './context';

export const App = (): JSX.Element => {
  /**
   * Those next three lines are useful to display a popup
   * if the client code has been updated. This trigger a signal
   * via an EventBus initiated in the service worker and
   * the couple line above.
   */
  const { zoomState, updateZoom } = useZoom();
  document.addEventListener('keydown', (e) => {
    if (!zoomState.overrideNative) return;
    if (!(e.ctrlKey || e.metaKey)) return;

    if (e.key === '=') {
      updateZoom('increase');
      e.preventDefault();
    } else if (e.key == '-') {
      updateZoom('decrease');
      e.preventDefault();
    }
  });

  return (
    <div class="text-neutral-900 dark:text-neutral-50 bg-neutral-100 dark:bg-neutral-950 relative flex h-screen flex-col overflow-auto font-sans">
      <Router
        root={(props) => (
          <AppContextProvider>
            <Suspense>{props.children}</Suspense>
          </AppContextProvider>
        )}
      >
        <Route path={['/', '/:user/:repl']} component={Edit} />
        <Route path="/:user" component={Home} />
        <Route path="/login" component={Login} />
      </Router>

      <Show when={eventBus()} children={<Update onDismiss={() => setEventBus(false)} />} />
    </div>
  );
};


================================================
FILE: packages/playground/src/components/header.tsx
================================================
import Dismiss from 'solid-dismiss';
import { A } from '@solidjs/router';
import { Icon } from 'solid-heroicons';
import { unwrap } from 'solid-js/store';
import { onCleanup, createSignal, Show, ParentComponent, children } from 'solid-js';
import { share, link, arrowDownTray, xCircle, bars_3, moon, sun } from 'solid-heroicons/outline';
import { exportToZip } from '../utils/exportFiles';
import { ZoomDropdown } from './zoomDropdown';
import { API, useAppContext } from '../context';
import { Button, LinkButton } from 'solid-repl/src/components/ui/Button';

import logo from '../assets/logo.svg?url';

export const Header: ParentComponent<{
  compiler?: Worker;
  fork?: () => void;
  share: () => Promise<string>;
}> = (props) => {
  const [copy, setCopy] = createSignal(false);
  const context = useAppContext()!;
  const [showMenu, setShowMenu] = createSignal(false);
  const [showProfile, setShowProfile] = createSignal(false);
  const resolved = children(() => props.children);
  let menuBtnEl!: HTMLButtonElement;
  let profileBtn!: HTMLButtonElement;

  function shareLink() {
    props.share().then((url) => {
      navigator.clipboard.writeText(url).then(() => {
        setCopy(true);
        setTimeout(setCopy, 750, false);
      });
    });
  }

  window.addEventListener('resize', closeMobileMenu);
  onCleanup(() => {
    window.removeEventListener('resize', closeMobileMenu);
  });

  function closeMobileMenu() {
    setShowMenu(false);
  }

  const menuButtonClasses = (show: boolean) => ({
    'rounded-none active:bg-gray-300 hover:bg-gray-300 dark:hover:text-black': show,
  });

  return (
    <header class="top-0 z-12 gap-x-4 p-1 px-2 bg-neutral-100 dark:bg-neutral-950 sticky flex items-center text-sm">
      <A href={`/${context.profile()}`}>
        <img src={logo} alt="solid-js logo" class="w-8" />
      </A>
      {resolved() || (
        <h1 class="leading-0 uppercase tracking-widest">
          Solid<b>JS</b> Playground
        </h1>
      )}
      <div class="space-x-2 ml-auto flex items-center">
        <Dismiss
          classList={{
            'absolute top-[53px] right-[10px] z-10 w-fit': showMenu(),
            'flex flex-col justify-center border-neutral-200 bg-white dark:border-neutral-700 rounded-lg border shadow-lg dark:bg-neutral-900':
              showMenu(),
            'hidden': !showMenu(),
          }}
          class="md:space-x-2 md:flex md:flex-row md:items-center"
          menuButton={() => menuBtnEl}
          open={showMenu}
          setOpen={setShowMenu}
          show
        >
          <Button onClick={context.toggleDark} classList={menuButtonClasses(showMenu())} title="Toggle dark mode">
            <Show when={context.dark()} fallback={<Icon path={moon} class="h-6" />}>
              <Icon path={sun} class="h-6" />
            </Show>
            <span class="text-sm md:sr-only">{context.dark() ? 'Light' : 'Dark'} mode</span>
          </Button>

          <Show when={context.tabs()}>
            <Button
              onClick={() => exportToZip(unwrap(context.tabs())!)}
              classList={menuButtonClasses(showMenu())}
              title="Export to Zip"
            >
              <Icon path={arrowDownTray} class="h-6" style={{ margin: '0' }} />
              <span class="text-sm md:sr-only">Export to Zip</span>
            </Button>
          </Show>

          <ZoomDropdown showMenu={showMenu()} />

          <Button
            onClick={shareLink}
            classList={{
              ...menuButtonClasses(showMenu()),
              'opacity-80 hover:opacity-100': !copy(),
              'text-green-100': copy() && !showMenu(),
            }}
            title="Share with a minified link"
          >
            <Icon class="h-6" path={copy() ? link : share} />
            <span class="text-sm md:sr-only">{copy() ? 'Copied to clipboard' : 'Share'}</span>
          </Button>

          <LinkButton
            href="https://github.com/solidjs/solid-playground"
            target="_blank"
            classList={menuButtonClasses(showMenu())}
            title="Github"
            class="cursor-alias"
          >
            <Icon
              viewBox="0 0 96 96"
              class="h-6"
              path={{
                path: (
                  <path
                    fill-rule="evenodd"
                    clip-rule="evenodd"
                    d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
                  />
                ),
                outline: false,
                mini: false,
              }}
            />
            <span class="text-sm md:sr-only">Github</span>
          </LinkButton>
        </Dismiss>
        <Button
          type="button"
          classList={{
            'border-white border': showMenu(),
          }}
          class="md:hidden"
          variant="ghost"
          title="Mobile Menu Button"
          ref={menuBtnEl}
        >
          <Show when={showMenu()} fallback={<Icon path={bars_3} class="h-6 w-6" />}>
            <Icon path={xCircle} class="h-[22px] w-[22px]" />
          </Show>
          <span class="sr-only">Show menu</span>
        </Button>
        <div class="relative flex shrink-0 cursor-pointer items-center leading-snug">
          <Show
            when={context.user()?.avatar}
            fallback={
              <a
                class="mx-1 px-3 py-1.5 text-white rounded-md bg-solidc text-sm hover:bg-solidc/90"
                href={`${API}/auth/login?redirect=${window.location.origin}/login?auth=success`}
                rel="external"
              >
                Login
              </a>
            }
          >
            <button ref={profileBtn}>
              <img crossOrigin="anonymous" src={context.user()?.avatar} class="h-8 w-8 rounded-full" />
            </button>
            <Dismiss menuButton={() => profileBtn} open={showProfile} setOpen={setShowProfile}>
              <div class="right-0 mt-2 bg-white border-neutral-200 dark:border-neutral-700 dark:bg-neutral-900 absolute flex flex-col items-center justify-center overflow-hidden rounded-lg border shadow-lg">
                <a
                  class="w-full px-4 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 border-neutral-200 dark:border-neutral-700 whitespace-nowrap border-b text-center text-sm"
                  href={`/${context.profile()}`}
                >
                  {context.user()?.display}
                </a>
                <button
                  onClick={() => (context.token = '')}
                  class="w-full px-4 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 whitespace-nowrap text-center text-sm"
                >
                  Sign Out
                </button>
              </div>
            </Dismiss>
          </Show>
        </div>
      </div>
    </header>
  );
};


================================================
FILE: packages/playground/src/components/setupSolid.ts
================================================
import { typescript } from 'monaco-editor';

const solidTypes: Record<string, string> = import.meta.glob('/node_modules/{solid-js,csstype}/**/*.{d.ts,json}', {
  eager: true,
  query: '?raw',
  import: 'default',
});

for (const path in solidTypes) {
  typescript.typescriptDefaults.addExtraLib(solidTypes[path], `file://${path}`);
  typescript.javascriptDefaults.addExtraLib(solidTypes[path], `file://${path}`);
}

import repl from 'solid-repl/src/repl';
export default repl;


================================================
FILE: packages/playground/src/components/update.tsx
================================================
import type { Component } from 'solid-js';
import { Portal } from 'solid-js/web';

import { Icon } from 'solid-heroicons';
import { xMark } from 'solid-heroicons/outline';

export const Update: Component<{
  onDismiss: (...args: unknown[]) => unknown;
}> = (props) => {
  const mount = document.getElementById('update');

  return (
    <Portal mount={mount!}>
      <div class="z-10 border-neutral-200 bg-white px-6 py-4 dark:border-neutral-700 dark:text-white dark:bg-neutral-900 relative max-w-sm rounded-lg border text-solidc shadow-lg">
        <button
          title="close"
          onClick={props.onDismiss}
          class="top-2 right-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 p-1 absolute rounded-md"
        >
          <Icon path={xMark} class="h-5" />
        </button>
        <p class="pr-6 font-semibold">There's a new update available.</p>
        <p class="mt-2 text-sm opacity-80">
          Refresh your browser or click the button below to get the latest update of the REPL.
        </p>
        <button
          onClick={() => location.reload()}
          class="mt-4 px-3 py-1.5 text-white rounded-md bg-solidc text-sm hover:bg-solidc/90"
        >
          Refresh
        </button>
      </div>
    </Portal>
  );
};


================================================
FILE: packages/playground/src/components/zoomDropdown.tsx
================================================
import { Icon } from 'solid-heroicons';
import { magnifyingGlassPlus, minus, plus } from 'solid-heroicons/outline';
import Dismiss from 'solid-dismiss';
import { Component, createSignal, createEffect } from 'solid-js';
import { useZoom } from 'solid-repl/src/hooks/useZoom';
import { Button } from 'solid-repl/src/components/ui/Button';
import { Checkbox } from 'solid-repl/src/components/ui/Checkbox';

export const ZoomDropdown: Component<{ showMenu: boolean }> = (props) => {
  const [open, setOpen] = createSignal(false);
  const { zoomState, updateZoom, setZoomState } = useZoom();
  const popupDuration = 1250;
  let containerEl!: HTMLDivElement;
  let prevZoom = zoomState.zoom;
  let timeoutId: number | null = null;
  let btnEl!: HTMLButtonElement;
  let prevFocusedEl: HTMLElement | null;
  let stealFocus = true;

  const onMouseMove = () => {
    stealFocus = true;
    window.clearTimeout(timeoutId!);
  };

  const onKeyDownContainer = (e: KeyboardEvent) => {
    if (!open()) return;

    if (e.key === 'Escape' && !stealFocus) {
      if (prevFocusedEl) {
        setOpen(false);
        prevFocusedEl.focus();
        stealFocus = true;
      }
      window.clearTimeout(timeoutId!);
    }

    if (!['Tab', 'Enter', 'Space'].includes(e.key)) return;
    stealFocus = false;
    prevFocusedEl = null;
    window.clearTimeout(timeoutId!);
  };

  createEffect(() => {
    if (prevZoom === zoomState.zoom) return;
    prevZoom = zoomState.zoom;

    if (stealFocus) {
      prevFocusedEl = document.activeElement as HTMLElement;
      btnEl.focus();
      stealFocus = false;
    }

    setOpen(true);

    window.clearTimeout(timeoutId!);

    timeoutId = window.setTimeout(() => {
      setOpen(false);

      stealFocus = true;
      if (prevFocusedEl) {
        prevFocusedEl.focus();
      }
    }, popupDuration);
  });

  createEffect(() => {
    if (!open()) {
      if (containerEl) {
        containerEl.removeEventListener('mouseenter', onMouseMove);
      }
      stealFocus = true;
    } else {
      if (containerEl) {
        containerEl.addEventListener('mouseenter', onMouseMove, { once: true });
      }
    }
  });

  return (
    <div
      class="relative"
      onKeyDown={onKeyDownContainer}
      onClick={() => {
        window.clearTimeout(timeoutId!);
      }}
      ref={containerEl}
      tabindex="-1"
    >
      <Button
        type="button"
        classList={{
          'rounded-none active:bg-gray-300 hover:bg-gray-300 dark:hover:text-black': props.showMenu,
          'bg-gray-300 dark:text-black': open() && props.showMenu,
        }}
        title="Scale editor to make text larger or smaller"
        ref={btnEl}
      >
        <Icon class="h-6" path={magnifyingGlassPlus} />
        <span class="text-sm md:sr-only">Scale Editor</span>
      </Button>
      <Dismiss menuButton={btnEl} open={open} setOpen={setOpen}>
        <div
          class="z-10 w-min border-neutral-200 bg-white p-4 dark:border-neutral-700 dark:bg-neutral-900 absolute rounded-lg border shadow-lg"
          classList={{
            'left-1/4': props.showMenu,
          }}
          style={{
            transform: 'translateX(calc(2rem - 100%))',
          }}
        >
          <div class="flex items-center">
            <button
              class="border-neutral-200 p-1.5 hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800 inline-flex items-center justify-center rounded-l-md border"
              aria-label="decrease font size"
              onClick={() => updateZoom('decrease')}
            >
              <Icon path={minus} class="h-4 w-4" />
            </button>
            <div class="w-20 border-neutral-200 px-3 py-1 dark:border-neutral-700 border-y text-center text-sm tabular-nums">
              {zoomState.zoom}%
            </div>
            <button
              class="mr-4 border-neutral-200 p-1.5 hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800 inline-flex items-center justify-center rounded-r-md border"
              aria-label="increase font size"
              onClick={() => updateZoom('increase')}
            >
              <Icon path={plus} class="h-4 w-4" />
            </button>
            <button
              class="border-neutral-200 px-3 py-1 hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800 rounded-md border text-sm"
              aria-label="reset font size"
              onClick={() => updateZoom('reset')}
            >
              Reset
            </button>
          </div>
          <div class="mt-3 space-y-2">
            <Checkbox
              label="Override browser zoom keyboard shortcut"
              checked={zoomState.overrideNative}
              onChange={(e) => setZoomState('overrideNative', e.currentTarget.checked)}
            />
            <Checkbox
              label="Scale iframe result"
              checked={zoomState.scaleIframe}
              onChange={(e) => setZoomState('scaleIframe', e.currentTarget.checked)}
            />
          </div>
        </div>
      </Dismiss>
    </div>
  );
};


================================================
FILE: packages/playground/src/context.tsx
================================================
import { Accessor, createContext, createResource, createSignal, ParentComponent, Resource, useContext } from 'solid-js';
import type { Tab } from 'solid-repl';
import { isDarkTheme } from './utils/isDarkTheme';

interface AppContextType {
  token: string;
  user: Resource<{ display: string; avatar: string } | undefined>;
  profile: Accessor<string>;
  tabs: Accessor<Tab[] | undefined>;
  setTabs: (x: Accessor<Tab[] | undefined> | undefined) => void;
  dark: Accessor<boolean>;
  toggleDark: () => void;
}

const AppContext = createContext<AppContextType>();

// export const API = 'http://localhost:8787';
// export const API = '/api';
export const API = 'https://api.solidjs.com';

export const AppContextProvider: ParentComponent = (props) => {
  const [token, setToken] = createSignal(localStorage.getItem('token') || '');
  const [user] = createResource(token, async (token) => {
    if (!token)
      return {
        display: '',
        avatar: '',
      };
    const result = await fetch(`${API}/profile`, {
      headers: {
        authorization: `Bearer ${token}`,
      },
    });
    const body = await result.json();
    return {
      display: body.display,
      avatar: body.avatar,
    };
  });

  const [dark, setDark] = createSignal(isDarkTheme());
  document.body.classList.toggle('dark', dark());

  let [tabsGetter, setTabs] = createSignal<Accessor<Tab[] | undefined>>();
  return (
    <AppContext.Provider
      value={{
        get token() {
          return token();
        },
        set token(x) {
          setToken(x);
          localStorage.setItem('token', x);
        },
        user,
        profile: () => user()?.display || 'anonymous',
        tabs() {
          const tabs = tabsGetter();
          if (!tabs) return undefined;
          return tabs();
        },
        setTabs(x) {
          setTabs(() => x);
        },
        dark,
        toggleDark() {
          let x = !dark();
          document.body.classList.toggle('dark', x);
          setDark(x);
          localStorage.setItem('dark', String(x));
        },
      }}
    >
      {props.children}
    </AppContext.Provider>
  );
};

export const useAppContext = () => useContext(AppContext);


================================================
FILE: packages/playground/src/index.tsx
================================================
import { render } from 'solid-js/web';
import { App } from './app';
import { registerServiceWorker } from './utils/serviceWorker';
import 'solid-repl/repl/main.css';
import 'virtual:uno.css';

render(() => <App />, document.querySelector('#app')!);

registerServiceWorker();


================================================
FILE: packages/playground/src/pages/edit.tsx
================================================
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import CompilerWorker from 'solid-repl/repl/compiler?worker';
import FormatterWorker from 'solid-repl/repl/formatter?worker';
import LinterWorker from 'solid-repl/repl/linter?worker';
import { batch, createEffect, createResource, createSignal, lazy, onCleanup, Show, Suspense } from 'solid-js';
import { useLocation, useMatch, useNavigate, useParams, useSearchParams } from '@solidjs/router';
import { API, useAppContext } from '../context';
import { debounce } from '@solid-primitives/scheduled';
import { decompressFromURL } from '@amoutonbrady/lz-string';
import { defaultTabs } from 'solid-repl/src';
import type { Tab } from 'solid-repl';
import type { APIRepl } from './home';
import { Header } from '../components/header';
import { Button } from 'solid-repl/src/components/ui/Button';

function parseHash<T>(hash: string, fallback: T): T {
  try {
    return JSON.parse(decompressFromURL(hash) || '');
  } catch {
    return fallback;
  }
}

const Repl = lazy(() => import('../components/setupSolid'));

window.MonacoEnvironment = {
  getWorker(_moduleId: unknown, label: string) {
    switch (label) {
      case 'css':
        return new cssWorker();
      case 'json':
        return new jsonWorker();
      case 'typescript':
      case 'javascript':
        return new tsWorker();
      default:
        return new editorWorker();
    }
  },
};

interface InternalTab extends Tab {
  _source: string;
  _name: string;
}
export const Edit = () => {
  const [searchParams] = useSearchParams();
  const scratchpad = useMatch(() => '/');
  const compiler = new CompilerWorker();
  const formatter = new FormatterWorker();
  const linter = new LinterWorker();

  const params = useParams<{ user: string; repl: string }>();
  const context = useAppContext()!;
  const navigate = useNavigate();
  const location = useLocation();

  let disableFetch: true | undefined;

  let readonly = () => !scratchpad() && context.profile() != params.user && !localStorage.getItem(params.repl);

  createEffect(() => {
    if (!scratchpad()) return;
    if (location.query.hash) {
      navigate(`/anonymous/${location.query.hash}`);
    } else if (location.hash) {
      const initialTabs = parseHash(location.hash.slice(1), defaultTabs);
      localStorage.setItem(
        'scratchpad',
        JSON.stringify({
          files: initialTabs.map((x) => ({ name: x.name, content: x.source })),
        }),
      );
      navigate('/', { replace: true });
    }
  });

  const mapTabs = (toMap: (Tab | InternalTab)[]): InternalTab[] =>
    toMap.map((tab) => {
      if ('_source' in tab) return tab;
      return {
        _name: tab.name,
        get name() {
          return this._name;
        },
        set name(name: string) {
          this._name = name;
          updateRepl();
        },
        _source: tab.source,
        get source() {
          return this._source;
        },
        set source(source: string) {
          this._source = source;
          updateRepl();
        },
      };
    });

  const [tabs, trueSetTabs] = createSignal<InternalTab[]>([]);
  const setTabs = (tabs: (Tab | InternalTab)[]) => trueSetTabs(mapTabs(tabs));
  context.setTabs(tabs);
  onCleanup(() => context.setTabs(undefined));

  const [current, setCurrent] = createSignal<string | undefined>(undefined, { equals: false });
  const [resource, { mutate }] = createResource<APIRepl, { repl: string | undefined; scratchpad: boolean }>(
    () => ({ repl: params.repl, scratchpad: !!scratchpad() }),
    async ({ repl, scratchpad }): Promise<APIRepl> => {
      if (disableFetch) {
        disableFetch = undefined;
        if (resource.latest) return resource.latest;
      }

      let output: APIRepl;
      if (scratchpad) {
        const myScratchpad = localStorage.getItem('scratchpad');
        if (!myScratchpad) {
          output = {
            files: defaultTabs.map((x) => ({
              name: x.name,
              content: x.source,
            })),
          } as APIRepl;
          localStorage.setItem('scratchpad', JSON.stringify(output));
        } else {
          output = JSON.parse(myScratchpad);
        }
      } else {
        output = await fetch(`${API}/repl/${repl}`, {
          headers: { authorization: context.token ? `Bearer ${context.token}` : '' },
        }).then((r) => r.json());
      }

      batch(() => {
        setTabs(
          output.files.map((x) => {
            return { name: x.name, source: x.content };
          }),
        );
        setCurrent(output.files[0].name);
      });

      return output;
    },
  );

  const reset = () => {
    batch(() => {
      setTabs(mapTabs(defaultTabs));
      setCurrent(defaultTabs[0].name);
    });
  };

  const publishScratchpad = async (title: string) => {
    const newRepl = {
      title,
      public: true,
      labels: [] as string[],
      version: '1.0',
      files: tabs().map((x) => ({ name: x.name, content: x.source })),
    };
    const response = await fetch(`${API}/repl`, {
      method: 'POST',
      headers: {
        'authorization': context.token ? `Bearer ${context.token}` : '',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newRepl),
    });
    if (response.status >= 400) {
      throw new Error(response.statusText);
    }
    const { id, write_token } = await response.json();
    if (write_token) {
      localStorage.setItem(id, write_token);
      const repls = localStorage.getItem('repls');
      if (repls) {
        localStorage.setItem('repls', JSON.stringify([...JSON.parse(repls), id]));
      } else {
        localStorage.setItem('repls', JSON.stringify([id]));
      }
    }
    mutate(() => ({
      id,
      title: newRepl.title,
      labels: newRepl.labels,
      files: newRepl.files,
      version: newRepl.version,
      public: newRepl.public,
      size: 0,
      created_at: '',
    }));
    const url = `/${context.profile()}/${id}`;
    disableFetch = true;
    navigate(url);
    return url;
  };

  const [forkPromptFor, setForkPromptFor] = createSignal<string | null>(null);
  const [forkDeclinedFor, setForkDeclinedFor] = createSignal<string | null>(null);
  const forkPromptOpen = () => forkPromptFor() === params.repl;
  const forkDeclined = () => forkDeclinedFor() === params.repl;

  const onUserEdit = () => {
    if (!readonly() || forkDeclined()) return;
    setForkPromptFor(params.repl);
  };

  const updateRepl = debounce(
    () => {
      if (readonly()) return;
      const files = tabs().map((x) => ({ name: x.name, content: x.source }));

      if (scratchpad()) {
        localStorage.setItem('scratchpad', JSON.stringify({ files }));
      }

      const repl = resource.latest;
      if (!repl) return;

      const loggedIn = context.token && params.user && context.profile() == params.user;

      if (loggedIn || localStorage.getItem(params.repl)) {
        fetch(`${API}/repl/${params.repl}`, {
          method: 'PUT',
          headers: {
            'authorization': context.token ? `Bearer ${context.token}` : '',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            ...(localStorage.getItem(params.repl) ? { write_token: localStorage.getItem(params.repl) } : {}),
            title: repl.title,
            version: repl.version,
            public: repl.public,
            labels: repl.labels,
            files,
          }),
        });
      }
    },
    !!scratchpad() ? 10 : 1000,
  );

  return (
    <>
      <Header
        compiler={compiler}
        fork={() => {}}
        share={async () => {
          if (scratchpad()) {
            const url = await publishScratchpad(`${context.user()?.display || 'Anonymous'}'s Scratchpad`);
            return `${window.location.origin}${url}`;
          } else if (readonly()) {
            const original = resource.latest;
            const url = await publishScratchpad(original?.title ? `${original.title} (fork)` : 'Forked Repl');
            return `${window.location.origin}${url}`;
          } else {
            return window.location.href;
          }
        }}
      >
        <Show when={resource() && (resource()?.title || (scratchpad() && context.token))}>
          <input
            class="w-96 border-transparent bg-transparent px-3 py-1.5 shrink rounded-md border transition focus:border-solidc focus:outline-none"
            value={resource()?.title ?? ''}
            placeholder={scratchpad() ? 'Name this repl to save it' : ''}
            onKeyDown={(e) => {
              if (e.key === 'Enter') e.currentTarget.blur();
            }}
            onChange={(e) => {
              const title = e.currentTarget.value;
              if (scratchpad() || readonly()) {
                if (title) publishScratchpad(title);
              } else {
                mutate((x) => x && { ...x, title });
                updateRepl();
              }
            }}
          />
        </Show>
      </Header>
      <Suspense
        fallback={
          <svg
            class="h-12 w-12 animate-spin text-neutral-500 m-auto"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
          >
            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
            <path
              class="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
            ></path>
          </svg>
        }
      >
        <Show when={resource()}>
          <Repl
            compiler={compiler}
            formatter={formatter}
            linter={linter}
            isHorizontal={searchParams.isHorizontal != undefined}
            dark={context.dark()}
            tabs={tabs()}
            setTabs={setTabs}
            reset={reset}
            current={current()}
            setCurrent={setCurrent}
            onUserEdit={onUserEdit}
            id="repl"
          />
        </Show>
      </Suspense>
      <Show when={forkPromptOpen()}>
        <div class="top-0 left-0 z-10 h-full w-full bg-black/50 fixed flex items-center justify-center">
          <div
            class="w-96 border-neutral-200 bg-white p-4 dark:border-neutral-700 dark:text-white rounded-lg border shadow-lg dark:bg-neutral-900"
            role="dialog"
            aria-modal="true"
            tabindex="-1"
          >
            <p class="font-semibold">Fork this repl?</p>
            <p class="mt-2 text-sm opacity-80">
              You're editing someone else's repl. Fork it to a new copy you can save.
            </p>
            <div class="mt-3 gap-2 flex justify-end">
              <Button
                onClick={() => {
                  setForkPromptFor(null);
                  setForkDeclinedFor(params.repl);
                }}
              >
                Cancel
              </Button>
              <Button
                variant="primary"
                onClick={() => {
                  setForkPromptFor(null);
                  const original = resource.latest;
                  publishScratchpad(original?.title ? `${original.title} (fork)` : 'Forked Repl');
                }}
              >
                Fork
              </Button>
            </div>
          </div>
        </div>
      </Show>
    </>
  );
};


================================================
FILE: packages/playground/src/pages/home.tsx
================================================
import { A, useParams } from '@solidjs/router';
import { Icon } from 'solid-heroicons';
import { eye, eyeSlash, plus, xMark } from 'solid-heroicons/outline';
import { createResource, createSignal, For, Show, Suspense } from 'solid-js';
import { createStore, produce } from 'solid-js/store';
import { API, useAppContext } from '../context';
import { Header } from '../components/header';
import { timeAgo } from '../utils/date';
import { Button } from 'solid-repl/src/components/ui/Button';

interface ReplFile {
  name: string;
  content: string;
}
export interface APIRepl {
  id: string;
  title: string;
  labels: string[];
  files: ReplFile[];
  version: string;
  public: boolean;
  size: number;
  created_at: string;
  updated_at?: string;
}
interface Repls {
  total: number;
  list: APIRepl[];
}

export const Home = () => {
  const params = useParams();
  const context = useAppContext()!;

  const [repls, setRepls] = createStore<Repls>({ total: 0, list: [] });
  const [resourceRepls] = createResource<Repls, { user: string | undefined }>(
    () => ({ user: params.user }),
    async ({ user }) => {
      if (!user && !context.token) return { total: 0, list: [] };
      let output = await fetch(`${API}/repl${user ? `/${user}/list` : '?'}`, {
        headers: {
          Authorization: `Bearer ${context.token}`,
        },
      }).then((r) => r.json());
      setRepls(output);
      return output;
    },
  );
  const get = <T,>(x: T) => {
    resourceRepls();
    return x;
  };

  const [open, setOpen] = createSignal<string>();

  return (
    <>
      <Header
        share={async () => {
          const url = new URL(document.location.origin);
          url.pathname = `/${params.user || context.profile()}`;
          return url.toString();
        }}
      />
      <div class="m-8">
        <Show
          when={params.user === context.profile()}
          fallback={<h1 class="mb-4 text-center text-sm">{`${params.user}'s`} Repls</h1>}
        >
          <div class="mb-8 flex flex-col align-middle">
            <A
              href="/"
              class="border-neutral-200 px-4 py-3 hover:bg-neutral-300 dark:border-neutral-700 dark:hover:bg-neutral-800 bg-neutral-200 dark:bg-neutral-900 mx-auto flex items-center rounded-lg border text-sm"
            >
              <Icon path={plus} class="mr-1 w-6" /> Create new REPL
            </A>
          </div>
        </Show>
        <table class="w-200 max-w-full border-spacing-0 mx-auto border-separate text-sm">
          <thead>
            <tr class="font-medium">
              <th class="w-1/2 border-neutral-200 px-3 py-2 dark:border-neutral-700 border-b text-left">Title</th>
              <th class="w-32 border-neutral-200 px-3 py-2 dark:border-neutral-700 border-b text-left last:text-right">
                Edited
              </th>
              <Show when={params.user === context.profile()}>
                <th class="w-20 border-neutral-200 px-3 py-2 dark:border-neutral-700 border-b text-right">Options</th>
              </Show>
            </tr>
          </thead>
          <tbody>
            <tr class="h-1" aria-hidden />
            <Suspense
              fallback={
                <tr class="h-10">
                  <td colspan="3" class="text-center">
                    <svg
                      class="mt-8 h-8 w-8 animate-spin text-neutral-500 mx-auto"
                      xmlns="http://www.w3.org/2000/svg"
                      fill="none"
                      viewBox="0 0 24 24"
                    >
                      <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                      <path
                        class="opacity-75"
                        fill="currentColor"
                        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                      ></path>
                    </svg>
                  </td>
                </tr>
              }
            >
              <For each={get(repls.list)}>
                {(repl, i) => (
                  <tr
                    class="group cursor-pointer"
                    onclick={(e) => {
                      if (e.target.tagName !== 'A') e.currentTarget.querySelector('a')!.click();
                    }}
                  >
                    <td class="px-3 py-2 group-hover:bg-neutral-200 dark:group-hover:bg-neutral-800 first:rounded-l-lg last:rounded-r-lg">
                      <A href={`/${params.user || context.profile()}/${repl.id}`}>{repl.title}</A>
                    </td>
                    <td class="px-3 py-2 group-hover:bg-neutral-200 dark:group-hover:bg-neutral-800 first:rounded-l-lg last:rounded-r-lg last:text-right">
                      {timeAgo(Date.now() - new Date(repl.updated_at || repl.created_at).getTime())}
                    </td>
                    <Show when={params.user === context.profile()}>
                      <td class="space-x-1 px-3 py-2 group-hover:bg-neutral-200 dark:group-hover:bg-neutral-800 text-right first:rounded-l-lg last:rounded-r-lg">
                        <Icon
                          path={repl.public ? eye : eyeSlash}
                          class="w-6 inline cursor-pointer"
                          onClick={async (e) => {
                            e.stopPropagation();
                            fetch(`${API}/repl/${repl.id}`, {
                              method: 'PATCH',
                              headers: {
                                'authorization': `Bearer ${context.token}`,
                                'Content-Type': 'application/json',
                              },
                              body: JSON.stringify({
                                public: !repl.public,
                              }),
                            });
                            setRepls(
                              produce((x) => {
                                x!.list[i()].public = !repl.public;
                              }),
                            );
                          }}
                        />
                        <Icon
                          path={xMark}
                          class="w-6 text-red-700 inline cursor-pointer"
                          onClick={(e) => {
                            e.stopPropagation();
                            setOpen(repl.id);
                          }}
                        />
                      </td>
                    </Show>
                  </tr>
                )}
              </For>
            </Suspense>
          </tbody>
        </table>
      </div>
      <Show when={!!open()}>
        <div
          class="top-0 left-0 z-10 h-full w-full bg-black/50 fixed flex items-center justify-center"
          onClick={(e) => {
            if (e.target !== e.currentTarget) return;
            setOpen(undefined);
          }}
          role="presentation"
        >
          <div
            class="w-96 border-neutral-200 bg-white p-4 dark:border-neutral-700 dark:text-white dark:bg-neutral-900 rounded-lg border shadow-lg"
            role="dialog"
            aria-modal="true"
            tabindex="-1"
          >
            <p>Are you sure you want to delete that?</p>
            <div class="mt-3 gap-2 flex justify-end">
              <Button onClick={() => setOpen(undefined)}>No</Button>
              <Button
                class="text-red-700 dark:text-red-400"
                onClick={() => {
                  fetch(`${API}/repl/${open()}`, {
                    method: 'DELETE',
                    headers: {
                      authorization: `Bearer ${context.token}`,
                    },
                  });
                  setRepls({
                    total: repls.total - 1,
                    list: repls.list.filter((x) => x.id !== open()),
                  });
                  setOpen(undefined);
                }}
              >
                Delete
              </Button>
            </div>
          </div>
        </div>
      </Show>
    </>
  );
};


================================================
FILE: packages/playground/src/pages/login.tsx
================================================
import { Navigate, useSearchParams } from '@solidjs/router';
import { useAppContext } from '../context';

export const Login = () => {
  const [params] = useSearchParams();
  const context = useAppContext()!;
  context.token = `${params.token ?? ''}`;
  return <Navigate href="/" />;
};


================================================
FILE: packages/playground/src/utils/date.ts
================================================
const formatter = new Intl.RelativeTimeFormat('en');
export const timeAgo = (ms: number): string => {
  const sec = Math.round(ms / 1000);
  const min = Math.round(sec / 60);
  const hr = Math.round(min / 60);
  const day = Math.round(hr / 24);
  const month = Math.round(day / 30);
  const year = Math.round(month / 12);
  if (sec < 10) {
    return 'just now';
  } else if (sec < 45) {
    return formatter.format(-sec, 'second');
  } else if (sec < 90 || min < 45) {
    return formatter.format(-min, 'minute');
  } else if (min < 90 || hr < 24) {
    return formatter.format(-hr, 'hour');
  } else if (hr < 36 || day < 30) {
    return formatter.format(-day, 'day');
  } else if (month < 18) {
    return formatter.format(-month, 'month');
  } else {
    return formatter.format(-year, 'year');
  }
};


================================================
FILE: packages/playground/src/utils/exportFiles.tsx
================================================
import pkg from '../../package.json';
import type { Tab } from 'solid-repl';
import dedent from 'dedent';

const viteConfigFile = dedent`
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";

export default defineConfig({
  plugins: [solidPlugin()],
  build: {
    target: "esnext",
    polyfillDynamicImport: false,
  },
});
`;
const tsConfig = JSON.stringify(
  {
    compilerOptions: {
      strict: true,
      module: 'ESNext',
      target: 'ESNext',
      jsx: 'preserve',
      esModuleInterop: true,
      sourceMap: true,
      allowJs: true,
      lib: ['es6', 'dom'],
      rootDir: 'src',
      moduleResolution: 'node',
      jsxImportSource: 'solid-js',
      types: ['solid-js', 'solid-js/dom'],
    },
  },
  null,
  2,
);

const indexHTML = (tabs: Tab[]) => dedent`
<html>
  <head>
    <title>Vite Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app"></div>

    <script type="module" src="./src/${tabs[0].name}"></script>
  </body>
</html>
`;

/**
 * This function will calculate the dependencies of the
 * package.json by using the imports list provided by the bundler,
 * and then generating the package.json itself, for the export
 */
function packageJSON(imports: string[]): string {
  const deps = imports.reduce(
    (acc, importPath): Record<string, string> => {
      const name = importPath.split('/')[0];
      if (!acc[name]) acc[name] = '*';
      return acc;
    },
    {} as Record<string, string>,
  );

  return JSON.stringify(
    {
      scripts: {
        start: 'vite',
        build: 'vite build',
      },
      dependencies: deps,
      devDependencies: {
        'vite': pkg.devDependencies['vite'],
        'vite-plugin-solid': pkg.devDependencies['vite-plugin-solid'],
      },
    },
    null,
    2,
  );
}

/**
 * This function will convert the tabs of the playground
 * into a ZIP formatted playground that can then be reimported later on
 */
export async function exportToZip(tabs: Tab[]): Promise<void> {
  const { default: JSZip } = await import('jszip');
  const zip = new JSZip();

  // basic structure
  zip.file('index.html', indexHTML(tabs));
  zip.file('vite.config.ts', viteConfigFile);
  zip.file('tsconfig.json', tsConfig);
  zip.folder('src');

  for (const tab of tabs) {
    if (tab.name == 'import_map.json') {
      zip.file('package.json', packageJSON(Object.keys(JSON.parse(tab.source))));
    } else {
      zip.file(`src/${tab.name}`, tab.source);
    }
  }

  const blob = await zip.generateAsync({ type: 'blob' });
  const url = URL.createObjectURL(blob);

  const anchor = (<a href={url} target="_blank" rel="noopener" download="solid-playground-project" />) as HTMLElement;
  document.body.prepend(anchor);
  anchor.click();
  anchor.remove();
}


================================================
FILE: packages/playground/src/utils/isDarkTheme.ts
================================================
export const isDarkTheme = () => {
  if (typeof window !== 'undefined') {
    if (window.localStorage) {
      const isDarkTheme = window.localStorage.getItem('dark');
      if (typeof isDarkTheme === 'string') {
        return isDarkTheme === 'true';
      }
    }

    const userMedia = window.matchMedia('(prefers-color-scheme: dark)');
    if (userMedia.matches) {
      return true;
    }
  }

  // Default theme is light.
  return false;
};


================================================
FILE: packages/playground/src/utils/serviceWorker.ts
================================================
import { register } from 'register-service-worker';
import { createSignal } from 'solid-js';

const [eventBus, setEventBus] = createSignal();

function registerServiceWorker(): void {
  if ('serviceWorker' in navigator && import.meta.env.PROD) {
    window.addEventListener('load', () => {
      register('/sw.js', {
        updated() {
          setEventBus(true);
        },
      });
    });
  }
}

if (import.meta.env.PROD) {
  navigator.serviceWorker?.addEventListener('message', (event) => {
    if (event.data.type == 'cache') {
      setEventBus(true);
    }
  });
}

export { eventBus, setEventBus, registerServiceWorker };


================================================
FILE: packages/playground/tsconfig.json
================================================
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"],
    "types": ["vite/client"]
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules/"]
}


================================================
FILE: packages/playground/unocss.config.ts
================================================
import { theme } from '@unocss/preset-wind3';
import { defineConfig } from 'unocss';
import sharedConfig from '../../uno.config.ts';

export default defineConfig({
  ...sharedConfig,
  theme: {
    ...(sharedConfig as any).theme,
    fontFamily: {
      sans: 'Gordita, ' + theme.fontFamily!.sans,
    },
  },
  content: {
    filesystem: ['./src/**/*.tsx', '../solid-repl/src/**/*.{tsx,ts}'],
  },
});


================================================
FILE: packages/playground/vite.config.ts
================================================
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import UnoCSS from 'unocss/vite';

export default defineConfig((env) => ({
  plugins: [solidPlugin(), UnoCSS()],
  define: {
    'process.env.NODE_DEBUG': 'false',
    ...(env.command == 'build' ? {} : { global: 'globalThis' }),
  },
  build: {
    target: 'esnext',
    rolldownOptions: {
      output: {
        entryFileNames: `assets/[name].js`,
        chunkFileNames: `assets/[name].js`,
        assetFileNames: `assets/[name].[ext]`,
      },
    },
  },
  worker: {
    rolldownOptions: {
      output: {
        entryFileNames: `assets/[name].js`,
      },
    },
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8787',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
}));


================================================
FILE: packages/solid-repl/build.ts
================================================
import { build } from 'esbuild';
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
import { copyFileSync } from 'fs-extra';

build({
  entryPoints: ['./repl/compiler.ts', './repl/formatter.ts', './repl/linter.ts', './repl/main.css'],
  outdir: './dist',
  minify: true,
  bundle: true,
  external: ['/Gordita-Medium.woff', '/Gordita-Regular.woff', '/Gordita-Bold.woff'],
  define: {
    'process.env.NODE_DEBUG': 'false',
    'preventAssignment': 'true',
  },
}).then(() => {
  const unoCSS_build = readFileSync('./uno.css');
  const generated_bundle = readFileSync('./dist/main.css');

  const output_bundle = Buffer.concat([generated_bundle, unoCSS_build]);

  writeFileSync('./dist/bundle.css', output_bundle);

  unlinkSync('./uno.css');
  unlinkSync('./dist/main.css');

  copyFileSync('./src/types.d.ts', './dist/types.d.ts');
});


================================================
FILE: packages/solid-repl/package.json
================================================
{
  "name": "solid-repl",
  "version": "0.26.0",
  "description": "Quickly discover what the solid compiler will generate from your JSX template",
  "homepage": "https://playground.solidjs.com",
  "author": "Alexandre Mouton-Brady",
  "repository": {
    "type": "git",
    "url": "https://github.com/solidjs/solid-playground.git"
  },
  "files": [
    "dist"
  ],
  "module": "dist/repl.jsx",
  "types": "src/types.d.ts",
  "scripts": {
    "build": "tsc -p tsconfig.build.json && unocss \"./src/**\" && jiti build.ts",
    "tsc": "tsc"
  },
  "dependencies": {
    "@floating-ui/dom": "^1.7.6",
    "@shikijs/monaco": "^4.0.2",
    "@solid-primitives/media": "^2.3.5",
    "@solid-primitives/scheduled": "^1.5.3",
    "dedent": "^1.7.2",
    "dockview-core": "^5.2.0",
    "shiki": "^4.0.2",
    "solid-dismiss": "^1.8.2",
    "solid-floating-ui": "^0.3.1",
    "solid-heroicons": "^3.2.4"
  },
  "devDependencies": {
    "@babel/core": "^7.29.0",
    "@babel/plugin-syntax-jsx": "^7.28.6",
    "@babel/preset-typescript": "^7.28.5",
    "@babel/standalone": "^7.29.2",
    "@babel/types": "^7.29.0",
    "@shikijs/langs": "^4.0.2",
    "@shikijs/themes": "^4.0.2",
    "@types/babel__standalone": "^7.1.9",
    "@types/dedent": "^0.7.2",
    "@types/fs-extra": "^11.0.4",
    "@unocss/cli": "^66.6.8",
    "@unocss/preset-wind3": "^66.6.8",
    "@unocss/reset": "^66.6.8",
    "babel-preset-solid": "^1.9.12",
    "esbuild": "^0.28.0",
    "eslint-solid-standalone": "<0.14.0",
    "fs-extra": "^11.3.4",
    "jiti": "^2.6.1",
    "monaco-editor": "^0.55.1",
    "prettier": "^3.8.3",
    "solid-js": "1.9.12",
    "typescript": "^6.0.3",
    "unocss": "^66.6.8"
  },
  "peerDependencies": {
    "solid-js": ">=1.7.0"
  }
}


================================================
FILE: packages/solid-repl/repl/compiler.ts
================================================
import type { Tab } from 'solid-repl';

import { transform } from '@babel/standalone';
// @ts-ignore
import babelPresetSolid from 'babel-preset-solid';

import dd from 'dedent';

function uid(str: string) {
  return Array.from(str)
    .reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0)
    .toString();
}

function babelTransform(filename: string, code: string, externals: Record<string, string>) {
  const handleImportee = (node: { value: string } | null | undefined) => {
    if (!node || typeof node.value !== 'string') return;
    const importee = node.value;
    if (importee.startsWith('.')) {
      node.value = 'solidrepl:' + importee;
    } else if (!importee.includes('://')) {
      if (!(importee in externals)) externals[importee] = `https://esm.sh/${importee}`;
    }
  };

  let { code: transformedCode } = transform(code, {
    plugins: [
      function importRewriter() {
        return {
          visitor: {
            Import(path: any) {
              handleImportee(path.parent.arguments[0]);
            },
            ImportDeclaration(path: any) {
              handleImportee(path.node.source);
            },
            ExportAllDeclaration(path: any) {
              handleImportee(path.node.source);
            },
            ExportNamedDeclaration(path: any) {
              handleImportee(path.node.source);
            },
          },
        };
      },
    ],
    presets: [
      [babelPresetSolid, { generate: 'dom', hydratable: false }],
      ['typescript', { onlyRemoveTypeImports: true }],
    ],
    filename: filename + '.tsx',
  });

  return transformedCode!.replace('render(', 'window.dispose = render(');
}

function transformTab(tab: Tab, externals: Record<string, string>): string {
  if (tab.name.endsWith('.css')) {
    const id = uid(tab.name);
    return dd`
      (() => {
        let stylesheet = document.getElementById('${id}');
        if (!stylesheet) {
          stylesheet = document.createElement('style')
          stylesheet.setAttribute('id', '${id}')
          document.head.appendChild(stylesheet)
        }
        const styles = document.createTextNode(\`${tab.source.replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\`)
        stylesheet.innerHTML = ''
        stylesheet.appendChild(styles)
      })()
    `;
  }
  return babelTransform(tab.name, tab.source, externals);
}

function compile(tabs: Tab[], event: string) {
  const externals: Record<string, string> = {};
  const compiled: Record<string, string> = {};
  for (const tab of tabs) {
    const key = `./${tab.name.replace(/\.(tsx|jsx)$/, '')}`;
    compiled[key] = transformTab(tab, externals);
  }
  return { event, compiled, externals };
}

function babel(tab: Tab, compileOpts: any) {
  const { code } = transform(tab.source, {
    presets: [
      [babelPresetSolid, compileOpts],
      ['typescript', { onlyRemoveTypeImports: true }],
    ],
    filename: tab.name,
  });
  return { event: 'BABEL', compiled: code };
}

self.addEventListener('message', ({ data }) => {
  const { event, tabs, tab, compileOpts } = data;

  try {
    if (event === 'BABEL') {
      self.postMessage(babel(tab, compileOpts));
    } else if (event === 'ROLLUP') {
      self.postMessage(compile(tabs, event));
    }
  } catch (e) {
    self.postMessage({ event: 'ERROR', error: e });
  }
});

export {};


================================================
FILE: packages/solid-repl/repl/formatter.ts
================================================
import { format as prettierFormat } from 'prettier/standalone';
import * as prettierPluginBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';

function format(code: string) {
  return prettierFormat(code, {
    parser: 'babel-ts',
    plugins: [prettierPluginBabel, prettierPluginEstree as any],
  });
}

self.addEventListener('message', async ({ data }) => {
  const { event, code } = data;

  switch (event) {
    case 'FORMAT':
      self.postMessage({
        event: 'FORMAT',
        code: await format(code),
      });
      break;
  }
});

export {};


================================================
FILE: packages/solid-repl/repl/linter.ts
================================================
import { verify, verifyAndFix } from 'eslint-solid-standalone';
import type { Linter } from 'eslint-solid-standalone';
import type { editor } from 'monaco-editor';

export interface LinterWorkerPayload {
  event: 'LINT' | 'FIX';
  code: string;
  ruleSeverityOverrides?: Record<string, Linter.Severity>;
}

const messagesToMarkers = (messages: Array<Linter.LintMessage>): Array<editor.IMarkerData> => {
  if (messages.some((m) => m.fatal)) return []; // no need for any extra highlights on parse errors
  return messages.map((m) => ({
    startLineNumber: m.line,
    endLineNumber: m.endLine ?? m.line,
    startColumn: m.column,
    endColumn: m.endColumn ?? m.column,
    message: `${m.message}\neslint(${m.ruleId})`,
    severity: m.severity === 2 ? 8 /* error */ : 4 /* warning */,
  }));
};

self.addEventListener('message', ({ data }: MessageEvent<LinterWorkerPayload>) => {
  const { event } = data;
  try {
    if (event === 'LINT') {
      const { code, ruleSeverityOverrides } = data;
      self.postMessage({
        event: 'LINT' as const,
        markers: messagesToMarkers(verify(code, ruleSeverityOverrides)),
      });
    } else if (event === 'FIX') {
      const { code, ruleSeverityOverrides } = data;
      const fixReport = verifyAndFix(code, ruleSeverityOverrides);
      self.postMessage({
        event: 'FIX' as const,
        markers: messagesToMarkers(fixReport.messages),
        output: fixReport.output,
        fixed: fixReport.fixed,
      });
    }
  } catch (e) {
    console.error(e);
    self.postMessage({ event: 'ERROR' as const, error: e });
  }
});


================================================
FILE: packages/solid-repl/repl/main.css
================================================
@import url('@unocss/reset/tailwind.css');

@font-face {
  font-family: 'Gordita';
  src: url('/Gordita-Regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: 'Gordita';
  src: url('/Gordita-Bold.woff') format('woff');
  font-weight: 700;
  font-style: normal;
}

@font-face {
  font-family: 'Gordita';
  src: url('/Gordita-Medium.woff') format('woff');
  font-weight: 500;
  font-style: normal;
}

div[contenteditable='true']:focus {
  outline: none !important;
}

.dark {
  color-scheme: dark;
}

textarea.monaco-mouse-cursor-text:focus {
  box-shadow: unset;
}

#app .dockview-theme-abyss-spaced {
  padding-top: 0;
  --dv-color-abyss-dark: theme('colors.neutral.100');
  --dv-color-abyss: #ffffff;
  --dv-color-abyss-light: theme('colors.neutral.100');
  --dv-color-abyss-lighter: theme('colors.neutral.100');
  --dv-color-abyss-accent: theme('colors.medium');
  --dv-color-abyss-primary-text: theme('colors.neutral.800');
  --dv-color-abyss-secondary-text: theme('colors.neutral.400');
}

.dark #app .dockview-theme-abyss-spaced {
  --dv-color-abyss-dark: theme('colors.neutral.950');
  --dv-color-abyss: theme('colors.neutral.900');
  --dv-color-abyss-light: theme('colors.neutral.800/50');
  --dv-color-abyss-lighter: theme('colors.neutral.800/50');
  --dv-color-abyss-accent: theme('colors.medium');
  --dv-color-abyss-primary-text: theme('colors.neutral.100');
  --dv-color-abyss-secondary-text: theme('colors.neutral.400');
}

#app .dockview-theme-abyss-spaced .dv-groupview.dv-active-group {
  border: 2px solid var(--dv-color-abyss-accent);
}

#app .dockview-theme-abyss-spaced .dv-groupview.dv-inactive-group {
  border: 2px solid transparent;
}


================================================
FILE: packages/solid-repl/src/components/CompileMode.tsx
================================================
import { Component, Setter } from 'solid-js';
import { Label } from './ui/Label';
import { Input } from './ui/Input';

export const compileOptions = {
  SSR: { generate: 'ssr', hydratable: true },
  DOM: { generate: 'dom', hydratable: false },
  HYDRATABLE: { generate: 'dom', hydratable: true },
  UNIVERSAL: {
    generate: 'universal',
    hydratable: false,
    moduleName: 'solid-universal-module' as string,
  },
} as const;

interface CompileModeProps {
  mode: (typeof compileOptions)[keyof typeof compileOptions];
  setMode: Setter<(typeof compileOptions)[keyof typeof compileOptions]>;
  universalModuleName: string;
  setUniversalModuleName: Setter<string>;
}

export const CompileMode: Component<CompileModeProps> = (props) => {
  return (
    <div class="p-2">
      <Label class="mb-1 block">Compile mode</Label>

      <div class="mt-1 space-y-1 text-sm">
        {(['DOM', 'SSR', 'HYDRATABLE'] as const).map((m) => (
          <label class="space-x-2 mr-auto block cursor-pointer">
            <input
              checked={props.mode === compileOptions[m]}
              class="text-solidc"
              onChange={[props.setMode, compileOptions[m]]}
              type="radio"
              name="dom"
            />
            <span>
              {m === 'DOM'
                ? 'Client side rendering'
                : m === 'SSR'
                  ? 'Server side rendering'
                  : 'Client side rendering with hydration'}
            </span>
          </label>
        ))}

        <label class="space-x-2 mr-auto block cursor-pointer">
          <input
            checked={props.mode.generate === 'universal'}
            class="text-solidc"
            onChange={[props.setMode, compileOptions.UNIVERSAL]}
            type="radio"
            name="dom"
          />
          <span>Universal Rendering & moduleName:</span>
          <Input
            onFocus={[props.setMode, compileOptions.UNIVERSAL]}
            onInput={(e) => {
              props.setUniversalModuleName(e.currentTarget.value);
            }}
            size="sm"
            inline
            class="ml-2"
            type="text"
            value={props.universalModuleName}
            name="moduleName"
          />
        </label>
      </div>
    </div>
  );
};


================================================
FILE: packages/solid-repl/src/components/editor/index.tsx
================================================
import { Component, createEffect, onMount, onCleanup, Show } from 'solid-js';
import { Uri, languages, editor as mEditor, KeyMod, KeyCode, typescript } from 'monaco-editor';
import { useZoom } from '../../hooks/useZoom';
import { throttle } from '@solid-primitives/scheduled';
import { bell, bellSlash, codeBracket } from 'solid-heroicons/outline';
import { register } from './setupSolid';
import { IconButton } from '../ui/IconButton';

const Editor: Component<{
  model: mEditor.ITextModel;
  disabled?: true;
  isDark?: boolean;
  withMinimap?: boolean;
  formatter?: Worker;
  linter?: Worker;
  displayErrors?: boolean;
  setDisplayErrors?: (value: boolean) => void;
  onDocChange?: (code: string) => void;
  onUserEdit?: () => void;
  onEditorReady?: (
    editor: mEditor.IStandaloneCodeEditor,
    monaco: {
      Uri: typeof Uri;
      editor: typeof mEditor;
    },
  ) => void;
}> = (props) => {
  let parent!: HTMLDivElement;
  let editor: mEditor.IStandaloneCodeEditor;

  const { zoomState } = useZoom();

  if (props.formatter) {
    languages.registerDocumentFormattingEditProvider('typescript', {
      async provideDocumentFormattingEdits(model) {
        props.formatter!.postMessage({
          event: 'FORMAT',
          code: model.getValue(),
          pos: editor.getPosition(),
        });

        return new Promise((resolve) => {
          props.formatter!.addEventListener(
            'message',
            ({ data: { code } }) => {
              resolve([
                {
                  range: model.getFullModelRange(),
                  text: code,
                },
              ]);
            },
            { once: true },
          );
        });
      },
    });
  }
  if (props.linter) {
    const listener = ({ data }: any) => {
      if (props.displayErrors) {
        const { event } = data;
        if (event === 'LINT') {
          mEditor.setModelMarkers(props.model, 'eslint', data.markers);
        } else if (event === 'FIX') {
          mEditor.setModelMarkers(props.model, 'eslint', data.markers);
          data.fixed && props.model.setValue(data.output);
        }
      }
    };
    props.linter.addEventListener('message', listener);
    onCleanup(() => props.linter?.removeEventListener('message', listener));
  }

  const runLinter = throttle((code: string) => {
    if (props.linter && props.displayErrors) {
      props.linter.postMessage({
        event: 'LINT',
        code,
      });
    }
  }, 250);

  // Initialize Monaco
  onMount(() => {
    editor = mEditor.create(parent, {
      model: null,
      fontFamily: 'Menlo, Monaco, "Courier New", monospace',
      automaticLayout: true,
      readOnly: props.disabled,
      fontSize: zoomState.fontSize,
      lineDecorationsWidth: 5,
      lineNumbersMinChars: 3,
      padding: { top: 15 },
      minimap: {
        enabled: props.withMinimap,
      },
      dropIntoEditor: {
        enabled: false,
      },
    });

    createEffect(() => {
      editor.updateOptions({ readOnly: !!props.disabled });
    });

    if (props.linter) {
      editor.addAction({
        id: 'eslint.executeAutofix',
        label: 'Fix all auto-fixable problems',
        contextMenuGroupId: '1_modification',
        contextMenuOrder: 3.5,
        run: (ed) => {
          const code = ed.getValue();
          if (code) {
            props.linter?.postMessage({
              event: 'FIX',
              code,
            });
          }
        },
      });
    }

    editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, () => {
      // auto-format
      editor.getAction('editor.action.formatDocument')?.run();
      // auto-fix problems
      props.displayErrors && editor.getAction('eslint.executeAutofix')?.run();
      editor.focus();
    });

    editor.onDidChangeModelContent((e) => {
      const code = editor.getValue();
      props.onDocChange?.(code);
      runLinter(code);
      if (!e.isFlush) props.onUserEdit?.();
    });
  });
  onCleanup(() => editor.dispose());

  createEffect(() => {
    editor.setModel(props.model);
    register();
  });

  createEffect(() => {
    mEditor.setTheme(props.isDark ? 'dark-plus' : 'light-plus');
  });

  createEffect(() => {
    const fontSize = zoomState.fontSize;
    editor.updateOptions({ fontSize });
  });

  createEffect(() => {
    if (props.disabled) return;
    typescript.typescriptDefaults.setDiagnosticsOptions({
      noSemanticValidation: !props.displayErrors,
      noSyntaxValidation: !props.displayErrors,
    });
  });

  createEffect(() => {
    if (props.displayErrors) {
      // run on mount and when displayLintMessages is turned on
      runLinter(editor.getValue());
    } else {
      // reset eslint markers when displayLintMessages is turned off
      mEditor.setModelMarkers(props.model, 'eslint', []);
    }
  });

  onMount(() => {
    props.onEditorReady?.(editor, { Uri, editor: mEditor });
  });

  return (
    <>
      <div class="min-h-0 min-w-0 p-0 flex-1" ref={parent} />
      <Show when={!props.disabled}>
        <div class="w-full border-t-1 border-neutral-200 bg-white px-2 dark:border-neutral-700 dark:bg-neutral-900 flex h-[30px] items-center justify-between">
          <div></div>
          <div class="space-x-1 flex items-center">
            <IconButton
              icon={props.displayErrors ? bell : bellSlash}
              size="sm"
              title={props.displayErrors ? 'Disable error reporting' : 'Enable error reporting'}
              onClick={() => props.setDisplayErrors?.(!props.displayErrors)}
            />
            <IconButton
              icon={codeBracket}
              size="sm"
              title="Format Document"
              onClick={() => editor.getAction('editor.action.formatDocument')?.run()}
            />
            <span class="text-neutral-500 dark:text-neutral-400 px-2 text-sm">TypeScript</span>
          </div>
        </div>
      </Show>
    </>
  );
};

export default Editor;


================================================
FILE: packages/solid-repl/src/components/editor/monacoTabs.tsx
================================================
import { createMemo, onCleanup } from 'solid-js';
import type { Tab } from 'solid-repl';
import { Uri, editor, IDisposable } from 'monaco-editor';

export const createMonacoTabs = (folder: string, tabs: () => Tab[]) => {
  const currentTabs = createMemo<Map<string, { model: editor.ITextModel; watcher: IDisposable }>>((prevTabs) => {
    const newTabs = new Map<string, { model: editor.ITextModel; watcher: IDisposable }>();
    for (const tab of tabs()) {
      const url = `file:///${folder}/${tab.name}`;
      const lookup = prevTabs?.get(url);
      if (!lookup) {
        const uri = Uri.parse(url);
        const model = editor.createModel(tab.source, undefined, uri);
        const watcher = model.onDidChangeContent(() => (tab.source = model.getValue()));
        newTabs.set(url, { model, watcher });
      } else {
        lookup.model.setValue(tab.source);
        lookup.watcher.dispose();
        lookup.watcher = lookup.model.onDidChangeContent(() => (tab.source = lookup.model.getValue()));
        newTabs.set(url, lookup);
      }
    }

    if (prevTabs) {
      for (const [old, lookup] of prevTabs) {
        if (!newTabs.has(old)) lookup.model.dispose();
      }
    }
    return newTabs;
  });
  onCleanup(() => {
    for (const lookup of currentTabs().values()) lookup.model.dispose();
  });

  return currentTabs;
};


================================================
FILE: packages/solid-repl/src/components/editor/setupSolid.ts
================================================
import { languages, editor, typescript } from 'monaco-editor';
import { shikiToMonaco } from '@shikijs/monaco';
import { createHighlighterCoreSync } from 'shiki/core';
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
import darkPlus from '@shikijs/themes/dark-plus';
import lightPlus from '@shikijs/themes/light-plus';
import tsx from '@shikijs/langs/tsx';
import css from '@shikijs/langs/css';
import html from '@shikijs/langs/html';
import json from '@shikijs/langs/json';

const jsEngine = createJavaScriptRegexEngine();

const compilerOptions: typescript.CompilerOptions = {
  strict: true,
  target: typescript.ScriptTarget.ESNext,
  module: typescript.ModuleKind.ESNext,
  moduleResolution: typescript.ModuleResolutionKind.NodeJs,
  jsx: typescript.JsxEmit.Preserve,
  jsxImportSource: 'solid-js',
  allowNonTsExtensions: true,
};

typescript.typescriptDefaults.setCompilerOptions(compilerOptions);
typescript.javascriptDefaults.setCompilerOptions(compilerOptions);

const loader = createHighlighterCoreSync({
  themes: [darkPlus, lightPlus],
  langs: [tsx, css, html, json],
  engine: jsEngine,
});

languages.register({ id: 'tsx' });
languages.register({ id: 'css' });
languages.register({ id: 'html' });
languages.register({ id: 'json' });

export function register() {
  shikiToMonaco(loader, {
    editor,
    languages: {
      ...languages,
      setTokensProvider: (id: string, provider: any) => {
        if (id === 'tsx') {
          languages.setTokensProvider('tsx', provider);
          languages.setTokensProvider('typescript', provider);
          languages.setTokensProvider('javascript', provider);
        } else languages.setTokensProvider(id, provider);
      },
    } as any,
  });
}

register();


================================================
FILE: packages/solid-repl/src/components/error.tsx
================================================
import { Component, createMemo, createSignal } from 'solid-js';

import { Icon } from 'solid-heroicons';
import { chevronDown, chevronRight, xMark } from 'solid-heroicons/solid';
import { IconButton } from './ui/IconButton';

export const Error: Component<{
  onDismiss: (...args: unknown[]) => unknown;
  message: string;
}> = (props) => {
  const lines = createMemo(() => props.message.split('\n'));
  const firstLine = () => lines()[0] ?? '';
  const stackTrace = () => lines().slice(1).join('\n');
  const [isOpen, setIsOpen] = createSignal(false);

  return (
    <div class="border-red-300 bg-red-50 p-2 dark:bg-red-900/20 relative border-t-2">
      <details class="text-red-800 dark:text-red-300" onToggle={(event) => setIsOpen(event.currentTarget.open)}>
        <summary class="pr-8 flex cursor-pointer items-center">
          <Icon class="mr-1 h-5 w-5 opacity-70" path={isOpen() ? chevronDown : chevronRight} />
          <code class="text-sm font-medium" innerText={firstLine()}></code>
        </summary>

        <pre class="mt-2 ml-6 overflow-auto whitespace-pre text-sm opacity-80">
          <code innerText={stackTrace()}></code>
        </pre>
      </details>
      <IconButton
        icon={xMark}
        class="top-2 right-2 hover:bg-red-200 dark:hover:bg-red-800/50 text-red-800 dark:text-red-300 absolute"
        size="sm"
        onClick={() => props.onDismiss()}
      />
    </div>
  );
};


================================================
FILE: packages/solid-repl/src/components/newTab.tsx
================================================
import { Component, createSignal, For, createMemo, onMount, Show } from 'solid-js';
import { Icon } from 'solid-heroicons';
import {
  magnifyingGlass,
  documentPlus,
  document as documentIcon,
  square_2Stack,
  chevronRight,
  arrowUpTray,
  ellipsisHorizontal,
} from 'solid-heroicons/outline';
import type { Tab } from 'solid-repl';
import { Input } from './ui/Input';
import { IconButton } from './ui/IconButton';
import { Label } from './ui/Label';
import { Menu, MenuItem } from './ui/Menu';
import { pencil, trash as trashIcon } from 'solid-heroicons/outline';
import Dismiss from 'solid-dismiss';

interface NewTabProps {
  tabs: Tab[];
  onOpenPane: (id: string) => void;
  onOpenFile: (name: string) => void;
  onNewFile: (name: string) => void;
  onUpload: (name: string, source: string) => void;
  onDeleteFile: (name: string) => void;
  onRenameFile: (oldName: string, newName: string) => void;
  onClose: () => void;
}

export const NewTab: Component<NewTabProps> = (props) => {
  const [query, setQuery] = createSignal('');
  const [selectedIndex, setSelectedIndex] = createSignal(0);
  const [renamingFile, setRenamingFile] = createSignal<string | null>(null);
  let inputRef!: HTMLInputElement;
  let fileInputRef!: HTMLInputElement;

  const categories = createMemo(() => {
    const q = query().toLowerCase();
    const sections: { title: string; items: any[] }[] = [];
    let count = 0;

    // Panes - Always show Preview and Output
    const paneItems = ['Preview', 'Output']
      .filter((p) => p.toLowerCase().includes(q))
      .map((p) => ({
        type: 'pane',
        id: p,
        label: p,
        icon: square_2Stack,
        globalIndex: count++,
      }));
    if (paneItems.length) {
      sections.push({ title: 'Panes', items: paneItems });
    }

    // Files
    const files = props.tabs.filter((t) => t.name.toLowerCase().includes(q));
    if (files.length > 0) {
      sections.push({
        title: 'Files',
        items: files.map((t) => ({
          type: 'file',
          id: t.name,
          label: t.name,
          icon: documentIcon,
          globalIndex: count++,
        })),
      });
    }

    // Actions
    const actions = [];
    if (q === '' || 'upload file'.includes(q)) {
      actions.push({
        type: 'action',
        id: 'upload',
        label: 'Upload File',
        icon: arrowUpTray,
      });
    }
    if (q !== '' && !props.tabs.some((t) => t.name.toLowerCase() === q)) {
      actions.push({
        type: 'new',
        id: q,
        label: `Create "${query()}"`,
        icon: documentPlus,
      });
    }

    if (actions.length > 0) {
      sections.push({
        title: 'Actions',
        items: actions.map((item) => ({ ...item, globalIndex: count++ })),
      });
    }

    return sections;
  });

  const allItems = createMemo(() => categories().flatMap((c) => c.items));

  const handleKeyDown = (e: KeyboardEvent) => {
    if (renamingFile()) return;

    const items = allItems();
    if (e.key === 'ArrowDown') {
      setSelectedIndex((i) => (i + 1) % items.length);
    } else if (e.key === 'ArrowUp') {
      setSelectedIndex((i) => (i - 1 + items.length) % items.length);
    } else if (e.key === 'Enter') {
      const selected = items[selectedIndex()];
      if (selected) {
        handleSelect(selected);
      }
    }
  };

  const handleSelect = (item: any) => {
    props.onClose();
    if (item.type === 'pane') {
      props.onOpenPane(item.id);
    } else if (item.type === 'file') {
      props.onOpenFile(item.id);
    } else if (item.type === 'new') {
      props.onNewFile(item.id);
    } else if (item.type === 'action') {
      if (item.id === 'upload') {
        fileInputRef.click();
      }
    }
  };

  const handleFileUpload = (e: Event) => {
    const file = (e.target as HTMLInputElement).files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const content = e.target?.result as string;
        props.onUpload(file.name, content);
        props.onClose();
      };
      reader.readAsText(file);
    }
  };

  const [activeMenu, setActiveMenu] = createSignal<string | null>(null);

  onMount(() => requestAnimationFrame(() => inputRef.focus()));

  return (
    <div class="h-full w-full bg-white px-6 dark:bg-neutral-900 pb-30 flex flex-col overflow-y-scroll">
      <input type="file" ref={fileInputRef} class="hidden" onChange={handleFileUpload} />
      <div class="w-full mx-auto max-w-2xl">
        <div class="top-0 z-20 bg-white py-6 dark:bg-neutral-900 sticky shrink-0">
          <Icon
            path={magnifyingGlass}
            class="left-4 h-5 w-5 text-neutral-400 pointer-events-none absolute top-1/2 -translate-y-1/2"
          />
          <Input
            ref={inputRef}
            type="text"
            class="w-full pl-10"
            placeholder="Search panes, files, or type a new filename..."
            value={query()}
            onInput={(e) => {
              setQuery(e.currentTarget.value);
              setSelectedIndex(0);
            }}
            onKeyDown={handleKeyDown}
          />
        </div>

        <For each={categories()}>
          {(category) => (
            <div class="space-y-1 flex flex-col">
              <Label class="z-10 bg-white px-3 py-2 dark:bg-neutral-900 border-neutral-200 dark:border-neutral-700 sticky top-[84px] border-b">
                {category.title}
              </Label>
              <For each={category.items}>
                {(item) => {
                  let btnRef: HTMLButtonElement | undefined;
                  const isActive = () => item.globalIndex === selectedIndex();
                  const isRenaming = () => renamingFile() === item.id;
                  return (
                    <div class="group relative">
                      <div
                        class="w-full p-2 flex items-center rounded-lg cursor-pointer text-sm transition-colors"
                        classList={{
                          'bg-neutral-200 dark:bg-neutral-700': isActive(),
                          'hover:bg-neutral-100 dark:hover:bg-neutral-800/50': !isActive() && !isRenaming(),
                          'hover:bg-neutral-200 dark:hover:bg-neutral-700': isActive(),
                        }}
                        onClick={() => !isRenaming() && handleSelect(item)}
                      >
                        <div
                          class="mr-3 h-8 w-8 flex shrink-0 items-center justify-center rounded-full"
                          classList={{
                            'bg-solidc/10 text-solidc dark:bg-neutral-600 dark:text-white': isActive(),
                            'bg-neutral-100 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400': !isActive(),
                          }}
                        >
                          <Icon path={item.icon} class="h-4 w-4" />
                        </div>
                        <div class="min-w-0 flex-1 text-left">
                          <Show when={isRenaming()} fallback={<div class="truncate">{item.label}</div>}>
                            <Input
                              autofocus
                              size="sm"
                              value={item.label}
                              onClick={(e) => e.stopPropagation()}
                              onKeyDown={(e) => {
                                if (e.key === 'Enter') {
                                  e.stopPropagation();
                                  setRenamingFile(null);
                                  props.onRenameFile(item.id, e.currentTarget.value);
                                } else if (e.key === 'Escape') {
                                  setRenamingFile(null);
                                }
                              }}
                              onBlur={(e) => {
                                if (isRenaming()) {
                                  setRenamingFile(null);
                                  props.onRenameFile(item.id, e.currentTarget.value);
                                }
                              }}
                            />
                          </Show>
                        </div>
                        <div class="ml-2 space-x-1 flex shrink-0 items-center">
                          <Show when={item.type === 'file' && !isActive() && !isRenaming()}>
                            <IconButton
                              ref={btnRef}
                              icon={ellipsisHorizontal}
                              class="p-1 opacity-0 group-hover:opacity-100"
                              size="sm"
                              onClick={(e) => {
                                e.stopPropagation();
                                setActiveMenu(activeMenu() === item.id ? null : item.id);
                              }}
                            />
                          </Show>
                          <Show when={isActive() && !isRenaming()}>
                            <Icon path={chevronRight} class="h-4 w-4" />
                          </Show>
                        </div>
                      </div>
                      <Show when={item.type === 'file'}>
                        <Dismiss
                          open={() => activeMenu() === item.id}
                          setOpen={(val) => {
                            if (!val) setActiveMenu(null);
                          }}
                          menuButton={() => btnRef}
                        >
                          <Menu class="right-0 mt-1 absolute top-full" onClose={() => setActiveMenu(null)}>
                            <MenuItem
                              label="Open"
                              onClick={() => {
                                handleSelect(item);
                                setActiveMenu(null);
                              }}
                            />
                            <MenuItem
                              label="Rename"
                              icon={pencil}
                              onClick={() => {
                                setRenamingFile(item.id);
                                setActiveMenu(null);
                              }}
                            />
                            <MenuItem
                              label="Delete"
                              icon={trashIcon}
                              variant="danger"
                              onClick={() => {
                                if (confirm(`Delete ${item.label}?`)) {
                                  props.onDeleteFile(item.id);
                                }
                                setActiveMenu(null);
                              }}
                            />
                          </Menu>
                        </Dismiss>
                      </Show>
                    </div>
                  );
                }}
              </For>
            </div>
          )}
        </For>
        <Show when={categories().length === 0}>
          <div class="mt-8 text-neutral-500 shrink-0 text-center text-sm">
            <p>No results found for "{query()}"</p>
          </div>
        </Show>
      </div>
    </div>
  );
};


================================================
FILE: packages/solid-repl/src/components/preview.tsx
================================================
import { Component, createEffect, JSX, onCleanup, onMount } from 'solid-js';
import { useZoom } from '../hooks/useZoom';
import { Orientation, SplitviewComponent } from 'dockview-core';
import { SolidPanelView } from '../dockview/solid';

const dispatchZoomKeyToParent = `
  document.addEventListener('keydown', (e) => {
    if (!(e.ctrlKey || e.metaKey)) return;
    if (!['=', '-'].includes(e.key)) return;
    window.parent.postMessage({ event: 'ZOOM_KEY', value: { key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey } }, '*');
    e.preventDefault();
  }, true);
`;

// Sandboxed iframes get a unique opaque origin, which causes two problems for chobitsu:
//   1. localStorage / sessionStorage throw on access (opaque origins have no storage).
//   2. chobitsu's getUrl()/getOrigin() fall back to parent.location.{href,origin} for
//      "about:" / "null"-origin pages, and that read is cross-origin and throws — which
//      blanks out Page.getResourceTree, so chii's Sources panel stays empty.
// We install in-memory storage shims and replace `parent` with a thin object that returns
// the iframe's own location while forwarding postMessage to the real parent (so we can
// still talk to the playground).
const sandboxShim = `
  (() => {
    const make = () => {
      const m = new Map();
      return {
        getItem: (k) => (m.has(k) ? m.get(k) : null),
        setItem: (k, v) => { m.set(k, String(v)); },
        removeItem: (k) => { m.delete(k); },
        clear: () => { m.clear(); },
        key: (i) => Array.from(m.keys())[i] ?? null,
        get length() { return m.size; },
      };
    };
    Object.defineProperty(window, 'localStorage', { value: make(), configurable: true });
    Object.defineProperty(window, 'sessionStorage', { value: make(), configurable: true });
    const realParent = window.parent;
    window.parent = {
      location: { href: location.href, origin: location.origin || 'about:srcdoc' },
      postMessage: (msg, target, transfer) => realParent.postMessage(msg, target, transfer),
    };
  })();
`;

const mainIframeScript = `
  (() => {
    let finisher = undefined;
    let cache = {};

    const buildModule = (name, source, sources) => {
      if (cache[name]) return cache[name];
      cache[name] = 'error:cyclic import';
      const out = source.replace(/(['"])solidrepl:([^'"]+)\\1/g, (_, q, rel) => {
        if (sources[rel] == null) return q + rel + q;
        return q + buildModule(rel, sources[rel], sources) + q;
      });
      const blob = new Blob([out], { type: 'text/javascript' });
      cache[name] = URL.createObjectURL(blob);
      return cache[name];
    };

    const handleCodeUpdate = (sources) => {
      if (!sources || typeof sources['./main'] !== 'string') return;

      window.dispose?.();
      window.dispose = undefined;

      if (document.getElementById('app')) document.getElementById('app').innerHTML = '';

      console.clear();

      document.getElementById('appsrc')?.remove();

      for (const url of Object.values(cache)) {
        if (typeof url === 'string' && url.startsWith('blob:')) URL.revokeObjectURL(url);
      }
      cache = {};

      const script = document.createElement('script');
      script.id = 'appsrc';
      script.type = 'module';
      finisher = () => {};
      script.onload = () => {
        if (finisher) finisher();
        finisher = undefined;
      };
      script.src = buildModule('./main', sources['./main'], sources);
      document.body.appendChild(script);

      const load = document.getElementById('load');
      if (load) load.remove();
    };

    const sendToDevtools = (message) => {
      window.parent.postMessage(JSON.stringify(message), '*');
    };
    let id = 0;
    const sendToChobitsu = (message) => {
      message.id = 'tmp' + ++id;
      chobitsu.sendRawMessage(JSON.stringify(message));
    };
    chobitsu.setOnMessage((message) => {
      if (message.includes('"id":"tmp')) return;
      window.parent.postMessage(message, '*');
    });

    let pageSource = '';
    const pageDomain = chobitsu.domain('Page');
    if (pageDomain) {
      pageDomain.getResourceContent = (params) => {
        if (params.frameId === '1') {
          return Promise.resolve({ base64Encoded: false, content: pageSource });
        }
        return Promise.resolve({ base64Encoded: false, content: '' });
      };
    }

    const handle = (data) => {
      try {
        const { event, value } = data;
        if (event === 'CODE_UPDATE') {
          const next = () => handleCodeUpdate(value);
          if (finisher !== undefined) finisher = next;
          else next();
        } else if (event === 'IMPORT_MAP') {
          document.getElementById('importmap')?.remove();
          const importMap = document.createElement('script');
          importMap.id = 'importmap';
          importMap.type = 'importmap';
          importMap.textContent = JSON.stringify({ imports: value });
          document.head.appendChild(importMap);
        } else if (event === 'DARK') {
          document.documentElement.classList.toggle('dark', value);
        } else if (event === 'PAGE_SOURCE') {
          pageSource = value;
        } else if (event === 'DEV') {
          chobitsu.sendRawMessage(data.data);
        } else if (event === 'LOADED') {
          sendToDevtools({
            method: 'Page.frameNavigated',
            params: {
              frame: { id: '1', mimeType: 'text/html', securityOrigin: parent.location.origin, url: parent.location.href },
              type: 'Navigation',
            },
          });
          sendToChobitsu({ method: 'Network.enable' });
          sendToDevtools({ method: 'Runtime.executionContextsCleared' });
          sendToChobitsu({ method: 'Runtime.enable' });
          sendToChobitsu({ method: 'Debugger.enable' });
          sendToChobitsu({ method: 'DOMStorage.enable' });
          sendToChobitsu({ method: 'DOM.enable' });
          sendToChobitsu({ method: 'CSS.enable' });
          sendToChobitsu({ method: 'Overlay.enable' });
          sendToDevtools({ method: 'DOM.documentUpdated' });
        }
      } catch (e) {
        console.error(e);
      }
    };

    window.addEventListener('message', (e) => handle(e.data));

    ${dispatchZoomKeyToParent}
  })();
`;

const iframeHtml = `<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link href="https://ga.jspm.io/npm:modern-normalize@3.0.1/modern-normalize.css" rel="stylesheet" />
    <style>
      html, body { position: relative; width: 100%; height: 100%; }
      body {
        color: #333; margin: 0; padding: 8px; box-sizing: border-box;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
        max-width: 100%;
      }
      .dark body { color: #e5e7eb; }
      .dark { color-scheme: dark; }
      input, button, select, textarea {
        padding: 0.4em; margin: 0 0 0.5em 0; box-sizing: border-box;
        border: 1px solid #ccc; border-radius: 2px;
      }
      button { color: #333; background-color: #f4f4f4; outline: none; }
      button:disabled { color: #999; }
      button:not(:disabled):active { background-color: #ddd; }
      button:focus { border-color: #666; }
    </style>
    <script>${sandboxShim}</script>
    <script src="https://cdn.jsdelivr.net/npm/chobitsu@1.8.6/dist/chobitsu.min.js"></script>
    <script>${mainIframeScript}</script>
  </head>
  <body>
    <div id="load" style="display: flex; height: 80vh; align-items: center; justify-content: center">
      <p style="font-size: 1.5rem">Loading the playground...</p>
    </div>
    <div id="app"></div>
  </body>
</html>`;

const useDevtoolsSrc = () => {
  const html = `
  <!DOCTYPE html>
  <html lang="en">
  <meta charset="utf-8">
  <title>DevTools</title>
  <style>
    @media (prefers-color-scheme: dark) {
      body {
        background-color: rgb(41 42 45);
      }
    }
  </style>
  <script>${dispatchZoomKeyToParent}</script>
  <meta name="referrer" content="no-referrer">
  <script src="https://unpkg.com/@ungap/custom-elements/es.js"></script>
  <script type="module" src="https://cdn.jsdelivr.net/npm/chii@1.15.5/public/front_end/entrypoints/chii_app/chii_app.js"></script>
  <body class="undocked" id="-blink-dev-tools">`;
  const devtoolsRawUrl = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
  onCleanup(() => URL.revokeObjectURL(devtoolsRawUrl));
  return `${devtoolsRawUrl}#?embedded=${encodeURIComponent(location.origin)}`;
};

export const Preview: Component<Props> = (props) => {
  const { zoomState } = useZoom();

  let iframe!: HTMLIFrameElement;
  let devtoolsIframe!: HTMLIFrameElement;
  let outerContainer!: HTMLDivElement;

  let devtoolsLoaded = false;
  let isIframeReady = false;

  const sendToIframe = (msg: any) => {
    if (!isIframeReady) return;
    iframe.contentWindow!.postMessage(msg, '*');
  };

  createEffect(() => {
    if (!props.reloadSignal) return;

    isIframeReady = false;
    iframe.srcdoc = iframeHtml;
  });

  const devtoolsSrc = useDevtoolsSrc();

  const styleScale = () => {
    const pointerEvents = props.pointerEvents ? 'inherit' : 'none';
    if (zoomState.scale === 100 || !zoomState.scaleIframe) return `pointer-events: ${pointerEvents};`;

    return `pointer-events: ${pointerEvents}; width: ${zoomState.scale}%; height: ${zoomState.scale}%; transform: scale(${
      zoomState.zoom / 100
    }); transform-origin: 0 0;`;
  };

  onMount(() => {
    const frameworkComponents: Record<string, () => JSX.Element> = {
      preview: () => (
        <iframe
          title="Solid REPL"
          class="h-full min-h-0 w-full min-w-0 bg-white p-0 dark:bg-neutral-900 block overflow-scroll"
          style={styleScale()}
          ref={iframe}
          srcdoc={iframeHtml}
          onload={() => {
            isIframeReady = true;

            if (devtoolsLoaded) sendToIframe({ event: 'LOADED' });
            sendToIframe({ event: 'PAGE_SOURCE', value: iframeHtml });
            sendToIframe({ event: 'IMPORT_MAP', value: props.importMap });
            if (props.code['./main']) sendToIframe({ event: 'CODE_UPDATE', value: props.code });
            sendToIframe({ event: 'DARK', value: props.isDark });
          }}
          // @ts-ignore
          sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals allow-pointer-lock"
        />
      ),
      devtools: () => (
        <iframe
          title="Devtools"
          class="h-full min-h-0 w-full min-w-0"
          style={`pointer-events: ${props.pointerEvents ? 'inherit' : 'none'}`}
          ref={devtoolsIframe}
          src={devtoolsSrc}
          onload={() => (devtoolsLoaded = true)}
          classList={{ block: props.devtools, hidden: !props.devtools }}
        />
      ),
    };
    const splitview = new SplitviewComponent(outerContainer, {
      orientation: Orientation.VERTICAL,

      createComponent: ({ id, name }) => {
        return new SolidPanelView(id, name, frameworkComponents[name]);
      },
    });
    splitview.addPanel({
      id: 'preview',
      component: 'preview',
      minimumSize: 100,
    });
    splitview.addPanel({
      id: 'devtools',
      component: 'devtools',
      minimumSize: 100,
      snap: true,
    });

    createEffect(() => {
      sendToIframe({ event: 'DARK', value: props.isDark });
    });

    createEffect(() => {
      sendToIframe({ event: 'IMPORT_MAP', value: props.importMap });
    });

    createEffect(() => {
      if (!props.code['./main']) return;
      sendToIframe({ event: 'CODE_UPDATE', value: props.code });
    });

    const messageListener = (event: MessageEvent) => {
      if (event.data?.event === 'ZOOM_KEY') {
        document.dispatchEvent(new KeyboardEvent('keydown', event.data.value));
        return;
      }
      if (event.source === iframe.contentWindow) {
        devtoolsIframe.contentWindow!.postMessage(event.data, '*');
      }
      if (event.source === devtoolsIframe.contentWindow) {
        iframe.contentWindow!.postMessage({ event: 'DEV', data: event.data }, '*');
      }
    };
    window.addEventListener('message', messageListener);
    onCleanup(() => window.removeEventListener('message', messageListener));

    createEffect(() => {
      localStorage.setItem('uiTheme', props.isDark ? '"dark"' : '"default"');

      if (!devtoolsLoaded) return;

      devtoolsIframe.contentWindow!.location.reload();
    });
  });

  return <div class="min-h-0 flex flex-1 flex-col" ref={outerContainer} classList={props.classList}></div>;
};

type Props = {
  importMap: Record<string, string>;
  classList?: {
    [k: string]: boolean | undefined;
  };
  code: Record<string, string>;
  reloadSignal: boolean;
  devtools: boolean;
  isDark: boolean;
  pointerEvents: boolean;
};


================================================
FILE: packages/solid-repl/src/components/repl.tsx
================================================
import { createSignal, createEffect, batch, onCleanup, createMemo, onMount, Show, createRoot, JSX } from 'solid-js';
import { unwrap } from 'solid-js/store';
import { Preview } from './preview';
import { Error } from './error';
import { throttle } from '@solid-primitives/scheduled';
import { editor, Uri } from 'monaco-editor';
import { createMonacoTabs } from './editor/monacoTabs';
import { NewTab } from './newTab';
import { CompileMode, compileOptions } from './CompileMode';
import { IconButton } from './ui/IconButton';

import Editor from './editor';
import type { Repl as ReplProps } from 'solid-repl/dist/repl';
import type { Tab } from 'solid-repl';
import { DockviewComponent, Orientation, GroupPanelPartInitParameters, themeAbyssSpaced } from 'dockview-core';
import { insert } from 'solid-js/web';
import { Icon } from 'solid-heroicons';
import { plus, trash, pencil, xMark } from 'solid-heroicons/outline';
import '../../node_modules/dockview-core/dist/styles/dockview.css';
import { Menu, MenuItem } from './ui/Menu';
import Dismiss from 'solid-dismiss';

const getImportMap = (tabs: Tab[]): Record<string, string> => {
  try {
    const rawImportMap = tabs.find((tab) => tab.name === 'import_map.json');
    return JSON.parse(rawImportMap?.source ?? '{}');
  } catch {
    return {};
  }
};

export const Repl: ReplProps = (props) => {
  const { compiler, formatter, linter } = props;
  let now: number;

  const [error, setError] = createSignal('');
  const [output, setOutput] = createSignal<Record<string, string>>({});
  const [universalModuleName, setUniversalModuleName] = createSignal('solid-universal-module');
  const [mode, setMode] = createSignal<(typeof compileOptions)[keyof typeof compileOptions]>(compileOptions.DOM);

  const userTabs = () => props.tabs.filter((tab) => tab.name != 'import_map.json');

  const [outputVisible, setOutputVisible] = createSignal(false);
  const [previewVisible, setPreviewVisible] = createSignal(false);
  const [importMap, setImportMap] = createSignal(getImportMap(props.tabs), {
    equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
  });

  const outputUri = Uri.parse(`file:///${props.id}/output_dont_import.ts`);
  const outputModel = editor.createModel('', 'typescript', outputUri);
  onCleanup(() => outputModel.dispose());

  const onCompilerMessage = ({ data }: any) => {
    const { event, compiled, externals, error } = data;
    if (event === 'ERROR') {
      console.error(error);
      return setError(error.message);
    } else setError('');

    if (event === 'BABEL') {
      outputModel.setValue(compiled);
    }

    if (event === 'ROLLUP') {
      const currentMap = { ...importMap() };
      for (const file in currentMap) {
        // Catch any `jspm.dev` URLs and migrate them to `esm.sh`
        if (currentMap[file] === `https://jspm.dev/${file}`) {
          currentMap[file] = `https://esm.sh/${file}`;
        }
        if (!(file in externals) && currentMap[file] === `https://esm.sh/${file}`) {
          delete currentMap[file];
        }
      }
      for (const file in externals) {
        if (!(file in currentMap)) {
          currentMap[file] = externals[file];
        }
      }
      console.log(`Compilation took: ${performance.now() - now}ms`);

      batch(() => {
        let tab = props.tabs.find((tab) => tab.name === 'import_map.json');
        if (!tab) {
          tab = {
            name: 'import_map.json',
            source: JSON.stringify(currentMap, null, 2),
          };
          props.setTabs(props.tabs.concat(tab));
        } else {
          tab.source = JSON.stringify(currentMap, null, 2);
          const { model } = monacoTabs().get(`file:///${props.id}/import_map.json`)!;
          model.setValue(tab.source);
        }

        setOutput(compiled);
        setImportMap(currentMap);
      });
    }
  };
  compiler.addEventListener('message', onCompilerMessage);
  onCleanup(() => compiler.removeEventListener('message', onCompilerMessage));

  /**
   * We need to debounce a bit the compilation because
   * it takes ~15ms to compile with the web worker...
   * Also, real time feedback can be stressful
   */
  const sendCompile = (message: any) => {
    now = performance.now();

    compiler.postMessage(message);
  };
  const applyRollupCompilation = throttle(sendCompile, 250);
  const applyBabelCompilation = throttle(sendCompile, 250);

  const compile = () => {
    if (previewVisible()) {
      applyRollupCompilation({
        event: 'ROLLUP',
        tabs: unwrap(userTabs()),
      });
    }
    if (outputVisible() && props.current?.endsWith('.tsx')) {
      let compileOpts = mode();
      if (compileOpts === compileOptions.UNIVERSAL) {
        compileOpts = {
          generate: 'universal',
          hydratable: false,
          moduleName: universalModuleName(),
        };
      }
      applyBabelCompilation({
        event: 'BABEL',
        tab: unwrap(props.tabs.find((tab) => tab.name == props.current)),
        compileOpts,
      });
    }
  };

  /**
   * The heart of the playground. This recompile on
   * every tab source changes.
   */
  createEffect(() => {
    if (!props.tabs.length) return;

    compile();
  });
  const monacoTabs = createMonacoTabs(props.id, () => props.tabs);
  const currentModel = createMemo(() => monacoTabs().get(`file:///${props.id}/${props.current}`)!.model);

  let ref!: HTMLDivElement;

  const [reloadSignal] = createSignal(false, { equals: false });
  const [devtoolsOpen] = createSignal(!props.hideDevtools);
  const [displayErrors, setDisplayErrors] = createSignal(true);

  onMount(() => {
    const newFile = (name: string) => {
      if (!name.trim()) return;
      const newTab = {
        name: name,
        source: '',
      };
      batch(() => {
        props.setTabs(props.tabs.concat(newTab));
        props.setCurrent(newTab.name);
      });
      dockview.addPanel({
        id: name,
        tabComponent: 'file',
        component: 'editor',
        params: {
          currentModel: monacoTabs().get(`file:///${props.id}/${name}`)!.model,
        },
      });
    };

    const deleteFile = (name: string) => {
      if (name === 'main.tsx') return;
      const newTabs = props.tabs.filter((tab) => tab.name !== name);
      const panel = dockview.getGroupPanel(name);
      panel?.api.close();
      batch(() => {
        props.setTabs(newTabs);
        if (props.current === name) {
          const mainPanel = dockview.getGroupPanel('main.tsx');
          mainPanel?.focus();
          props.setCurrent('main.tsx');
        }
      });
    };

    const renameFile = (oldName: string, newName: string) => {
      if (oldName === 'main.tsx') return;
      if (newName === oldName) return;
      if (!newName.trim()) return;
      const exists = props.tabs.some((tab) => tab.name === newName);
      if (exists) {
        alert('A file with that name already exists');
        return;
      }

      const tab = props.tabs.find((t) => t.name === oldName);
      if (!tab) return;

      const newTabs = props.tabs.map((t) => (t.name === oldName ? { ...t, name: newName } : t));

      batch(() => {
        props.setTabs(newTabs);
        if (props.current === oldName) {
          props.setCurrent(newName);
        }
      });

      // Update Panel ID if it exists? Dockview often requires adding/removing for ID change.
      // Easiest is to close old and open new at same position or just let user re-open.
      // But we can try to keep it simple: if open, close and reopen.
      const panel = dockview.getGroupPanel(oldName);
      if (panel) {
        panel.api.close();
        dockview.addPanel({
          id: newName,
          tabComponent: 'file',
          component: 'editor',
          params: {
            currentModel: monacoTabs().get(`file:///${props.id}/${newName}`)!.model,
          },
        });
      }
    };

    const dockview = new DockviewComponent(ref, {
      theme: themeAbyssSpaced,
      defaultTabComponent: 'default',
      createLeftHeaderActionComponent: () => {
        const element = (<div class="h-full px-1 flex items-center"></div>) as HTMLDivElement;
        let disposer: () => void;

        return {
          element,
          init: (params) => {
            createRoot((dispose) => {
              disposer = dispose;

              insert(element, () => (
                <IconButton
                  icon={plus}
                  class="h-[28px]"
                  size="sm"
                  onClick={() => {
                    const panel = dockview.getPanel('newTab');
                    if (panel) {
                      panel.focus();
                    } else {
                      params.group.focus();
                      dockview.addPanel({
                        id: 'newTab',
                        title: 'New Tab',
                        tabComponent: 'default',
                        component: 'newTab',
                      });
                    }
                  }}
                  title="New Tab"
                >
                  <span class="sr-only">New tab</span>
                </IconButton>
              ));
            });
          },
          dispose: () => disposer?.(),
        };
      },
      createTabComponent: (panel) => {
        const element = (<div class="h-full pl-2 flex items-center"></div>) as HTMLDivElement;
        let disposer: () => void;

        return {
          element,
          init: (params) => {
            const [showMenu, setShowMenu] = createSignal(false);
            const [menuPos, setMenuPos] = createSignal({ x: 0, y: 0 });

            createRoot((dispose) => {
              disposer = dispose;
              const [panelTitle, setPanelTitle] = createSignal(params.title);
              params.api.onDidTitleChange((e) => {
                setPanelTitle(e.title);
              });
              const isFile = panel.name == 'file';
              let containerRef: HTMLDivElement | undefined;

              insert(element, () => (
                <div
                  ref={containerRef}
                  class="h-full space-x-2 group flex items-center"
                  onContextMenu={(e) => {
                    if (!isFile) return;
                    e.preventDefault();
                    e.stopPropagation();
                    setMenuPos({ x: e.clientX, y: e.clientY });
                    setShowMenu(true);
                  }}
                >
                  <span class="truncate text-sm">{panelTitle()}</span>

                  <button
                    class="p-0.5 hover:bg-neutral-200 dark:hover:bg-neutral-700 ml-auto rounded-sm opacity-0 transition-opacity group-hover:opacity-100"
                    onClick={(e) => {
                      if (e.defaultPrevented) return;
                      e.preventDefault();
                      params.api.close();
                    }}
                    title="Close tab"
                  >
                    <Icon path={xMark} class="h-3 w-3 text-neutral-500" />
                  </button>

                  <Show when={isFile && showMenu()}>
                    <Dismiss
                      open={() => true}
                      setOpen={(val) => {
                        if (!val) setShowMenu(false);
                      }}
                      show
                      menuButton={containerRef}
                    >
                      <Menu
                        style={{ top: `${menuPos().y}px`, left: `${menuPos().x}px` }}
                        onClose={() => setShowMenu(false)}
                      >
                        <MenuItem
                          label="Rename"
                          icon={pencil}
                          onClick={() => {
                            const newName = prompt('Rename file to:', panelTitle());
                            if (newName) renameFile(panelTitle(), newName);
                            setShowMenu(false);
                          }}
                        />
                        <MenuItem
                          label="Delete"
                          icon={trash}
                          variant="danger"
                          onClick={() => {
                            if (confirm(`Delete ${panelTitle()}?`)) {
                              deleteFile(panelTitle());
                            }
                            setShowMenu(false);
                          }}
                        />
                      </Menu>
                    </Dismiss>
                  </Show>
                </div>
              ));
            });

            const hide = () => setShowMenu(false);
            window.addEventListener('click', hide);
            onCleanup(() => window.removeEventListener('click', hide));
          },
          dispose: () => disposer?.(),
        };
      },
      createRightHeaderActionComponent: () => {
        const element = (<div class="h-full px-1 flex items-center justify-end"></div>) as HTMLDivElement;
        let disposer: () => void;

        return {
          element,
          init: (params) => {
            const [isTSX, setIsTSX] = createSignal(false);
            params.group.api.onDidActivePanelChange((e) => {
              if (!e) return;
              setIsTSX(e.panel.id.endsWith('.tsx'));
            });
            createRoot((dispose) => {
              disposer = dispose;

              insert(element, () => (
                <Show when={isTSX()}>
                  <IconButton
                    icon={trash}
                    class="h-[28px]"
                    size="sm"
                    onClick={() => {
                      const confirmReset = confirm('Are you sure you want to reset the editor?');
                      if (!confirmReset) return;
                      props.reset();
                    }}
                    title="Reset Editor"
                  >
                    <span class="sr-only">Reset Editor</span>
                  </IconButton>
                </Show>
              ));
            });
          },
          dispose: () => disposer?.(),
        };
      },
      createComponent(options) {
        const element = (<div class="h-full flex flex-col"></div>) as HTMLDivElement;
        let disposer: () => void;
        let onInit: ((params: GroupPanelPartInitParameters) => (() => void) | void) | undefined;

        let component: (
          params: GroupPanelPartInitParameters['params'],
          x: GroupPanelPartInitParameters,
        ) => JSX.Element = () => null;

        switch (options.name) {
          case 'newTab':
            component = (_, params) => (
              <NewTab
                tabs={props.tabs}
                onOpenPane={(id) => {
                  const panel = dockview.getGroupPanel(id);
                  if (panel) {
                    panel.focus();
                  } else {
                    dockview.addPanel({
                      id,
                      tabComponent: 'default',
                      component: id.toLowerCase(),
                      renderer: id === 'Preview' ? 'always' : undefined,
                    });
                  }
                }}
                onOpenFile={(name) => {
                  const panel = dockview.getGroupPanel(name);
                  if (panel) {
                    panel.focus();
                  } else {
                    dockview.addPanel({
                      id: name,
                      tabComponent: 'file',
                      component: 'editor',
                      params: {
                        currentModel: monacoTabs().get(`file:///${props.id}/${name}`)!.model,
                      },
                    });
                  }
                }}
                onNewFile={newFile}
                onUpload={(name: string, source: string) => {
                  const newTab = { name, source };
                  batch(() => {
                    props.setTabs(props.tabs.concat(newTab));
                    props.setCurrent(newTab.name);
                  });
                  setTimeout(() => {
                    dockview.addPanel({
                      id: name,
                      tabComponent: 'file',
                      component: 'editor',
                      params: {
                        currentModel: monacoTabs().get(`file:///${props.id}/${name}`)!.model,
                      },
                    });
                  });
                }}
                onDeleteFile={deleteFile}
                onRenameFile={renameFile}
                onClose={() => {
                  params.api.close();
                }}
              />
            );
            break;
          case 'editor':
            component = (params) => (
              <Editor
                model={params.currentModel}
                onDocChange={(code: string) => {
                  if (params.currentModel.uri.path.includes('import_map.json')) {
                    const newImportMap = JSON.parse(code);
                    setImportMap(newImportMap);
                  } else {
                    compile();
                  }
                }}
                onUserEdit={props.onUserEdit}
                formatter={formatter}
                linter={linter}
                isDark={props.dark}
                withMinimap={false}
                displayErrors={displayErrors()}
                setDisplayErrors={setDisplayErrors}
              />
            );
            break;
          case 'preview':
            setPreviewVisible(true);
            onCleanup(() => {
              setPreviewVisible(false);
            });
            const [previewIsActive, setPreviewIsActive] = createSignal(false);
            component = () => (
              <Preview
                importMap={importMap()}
                code={output()}
                reloadSignal={reloadSignal()}
                devtools={devtoolsOpen()}
                isDark={props.dark}
                pointerEvents={previewIsActive()}
              />
            );
            onInit = (params) => {
              setPreviewIsActive(params.api.isActive);
              const disposable = params.api.onDidActiveChange((e) => {
                setPreviewIsActive(e.isActive);
              });
              return () => disposable.dispose();
            };
            break;
          case 'output':
            setOutputVisible(true);
            onCleanup(() => {
              setOutputVisible(false);
            });
            component = () => (
              <section class="min-h-0 min-w-0 divide-y-1 divide-slate-200 dark:divide-neutral-800 relative flex flex-1 flex-col">
                <Editor model={outputModel} isDark={props.dark} disabled withMinimap={false} />

                <CompileMode
                  mode={mode()}
                  setMode={setMode}
                  universalModuleName={universalModuleName()}
                  setUniversalModuleName={setUniversalModuleName}
                />
              </section>
            );
            break;
        }

        let onInitDisposer: (() => void) | undefined;

        return {
          element,
          init: (params) => {
            const result = onInit?.(params);
            if (result) onInitDisposer = result;
            createRoot((dispose) => {
              insert(element, () => component(params.params, params));
              disposer = dispose;
            });
          },
          dispose: () => {
            onInitDisposer?.();
            disposer?.();
          },
        };
      },
    });

    dockview.fromJSON({
      grid: {
        root: {
          type: 'branch',
          data: [
            {
              type: 'leaf',
              data: { views: ['main.tsx'], activeView: 'main.tsx', id: '1' },
              size: 400,
            },
            {
              type: 'leaf',
              data: { views: ['Preview', 'Output'], activeView: 'Preview', id: '2' },
              size: 250,
            },
          ],
          size: 480,
        },
        width: 1600,
        height: 480,
        orientation: Orientation.HORIZONTAL,
      },
      activeGroup: '1',
      panels: {
        Output: {
          id: 'Output',
          tabComponent: 'default',
          contentComponent: 'output',
        },
        Preview: {
          id: 'Preview',
          contentComponent: 'preview',
          tabComponent: 'default',
          renderer: 'always',
        },
        [props.current!]: {
          id: props.current!,
          tabComponent: 'file',
          contentComponent: 'editor',
          params: {
            currentModel: currentModel(),
          },
        },
      },
    });

    dockview.onDidActivePanelChange((e) => {
      if (!e) return;
      if ('currentModel' in (e.params ?? {})) {
        props.setCurrent(e.id);
      }
    });
  });

  return (
    <div class="h-full min-h-0 text-black dark:text-white flex flex-1 flex-col overflow-hidden font-sans">
      <div ref={ref} class="min-h-0 flex flex-1 flex-col" />
      <Show when={error()}>
        <Error message={error()} onDismiss={() => setError('')} />
      </Show>
    </div>
  );
};


================================================
FILE: packages/solid-repl/src/components/ui/Button.tsx
================================================
import { ParentComponent, JSX, splitProps } from 'solid-js';

const baseClasses =
  'flex items-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-solidc disabled:pointer-events-none disabled:opacity-50 space-x-2';

const variants = {
  primary: 'bg-solidc text-white hover:bg-solidc/90 px-4 py-2 justify-center',
  ghost: 'hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-700 dark:text-neutral-300 px-2 py-1.5',
};

export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: keyof typeof variants;
  active?: boolean;
}

export const Button: ParentComponent<ButtonProps> = (props) => {
  const [local, others] = splitProps(props, ['variant', 'active', 'class', 'classList', 'children']);

  return (
    <button
      {...others}
      class={`${baseClasses} ${variants[local.variant || 'ghost']} ${local.class || ''}`}
      classList={{
        'bg-neutral-200 dark:bg-neutral-700 text-black dark:text-white': local.active,
        ...local.classList,
      }}
    >
      {local.children}
    </button>
  );
};

export interface LinkButtonProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {
  variant?: keyof typeof variants;
  active?: boolean;
}

export const LinkButton: ParentComponent<LinkButtonProps> = (props) => {
  const [local, others] = splitProps(props, ['variant', 'active', 'class', 'classList', 'children']);

  return (
    <a
      {...others}
      class={`${baseClasses} ${variants[local.variant || 'ghost']} ${local.class || ''}`}
      classList={{
        'bg-neutral-200 dark:bg-neutral-700 text-black dark:text-white': local.active,
        ...local.classList,
      }}
    >
      {local.children}
    </a>
  );
};


================================================
FILE: packages/solid-repl/src/components/ui/Checkbox.tsx
================================================
import { Component, JSX, splitProps } from 'solid-js';

export interface CheckboxProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

export const Checkbox: Component<CheckboxProps> = (props) => {
  const [local, others] = splitProps(props, ['label', 'class', 'classList']);

  return (
    <label
      class={`space-x-2 dark:text-white flex cursor-pointer items-center ${local.class || ''}`}
      classList={local.classList}
    >
      <input
        type="checkbox"
        {...others}
        class="h-4 w-4 border-neutral-300 dark:border-neutral-700 dark:bg-neutral-800 shrink-0 cursor-pointer rounded-sm text-solidc focus:ring-solidc"
      />
      <span class="text-sm">{local.label}</span>
    </label>
  );
};


================================================
FILE: packages/solid-repl/src/components/ui/IconButton.tsx
================================================
import { Component, JSX } from 'solid-js';
import { Icon } from 'solid-heroicons';

export interface IconButtonProps {
  ref?: HTMLButtonElement;
  icon: any;
  onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
  title?: string;
  active?: boolean;
  disabled?: boolean;
  class?: string;
  size?: 'sm' | 'md' | 'lg';
  children?: JSX.Element;
}

const sizes = {
  sm: 'p-1',
  md: 'p-1.5',
  lg: 'p-2',
};

const iconSizes = {
  sm: 'h-4 w-4',
  md: 'h-5 w-5',
  lg: 'h-6 w-6',
};

export const IconButton: Component<IconButtonProps> = (props) => {
  return (
    <button
      ref={props.ref}
      type="button"
      onClick={props.onClick}
      title={props.title}
      disabled={props.disabled}
      class={`hover:bg-neutral-200 dark:hover:bg-neutral-700 inline-flex items-center justify-center rounded-md opacity-80 transition-all hover:opacity-100 disabled:pointer-events-none disabled:opacity-50 ${
        sizes[props.size || 'md']
      } ${props.class || ''}`}
      classList={{
        'bg-neutral-200 dark:bg-neutral-700 opacity-100': props.active,
      }}
    >
      <Icon path={props.icon} class={iconSizes[props.size || 'md']} />
      {props.children}
    </button>
  );
};


================================================
FILE: packages/solid-repl/src/components/ui/Input.tsx
================================================
import { Component, JSX, splitProps } from 'solid-js';

export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
  size?: 'sm' | 'md';
  inline?: boolean;
}

const sizes = {
  sm: 'px-2 py-1',
  md: 'px-3 py-2',
};

export const Input: Component<InputProps> = (props) => {
  const [local, others] = splitProps(props, ['class', 'size', 'inline']);

  return (
    <input
      {...others}
      class={`border-neutral-200 bg-transparent dark:border-neutral-700 rounded-md border text-sm transition-colors duration-200 focus:border-solidc focus:outline-none dark:focus:border-solidc ${
        local.inline ? 'inline-block w-auto' : 'block w-full'
      } ${sizes[local.size || 'md']} ${local.class || ''}`}
    />
  );
};


================================================
FILE: packages/solid-repl/src/components/ui/Label.tsx
================================================
import { ParentComponent, JSX, splitProps } from 'solid-js';

export const Label: ParentComponent<JSX.LabelHTMLAttributes<HTMLLabelElement>> = (props) => {
  const [local, others] = splitProps(props, ['class', 'children']);

  return (
    <label {...others} class={`text-neutral-500 dark:text-neutral-400 text-sm font-semibold ${local.class || ''}`}>
      {local.children}
    </label>
  );
};


================================================
FILE: packages/solid-repl/src/components/ui/Menu.tsx
================================================
import { ParentComponent, Component, Show } from 'solid-js';
import { Icon } from 'solid-heroicons';

export interface MenuProps {
  onClose: () => void;
  class?: string;
  style?: JSX.CSSProperties;
}

export const Menu: ParentComponent<MenuProps> = (props) => {
  return (
    <div
      class={`min-w-32 border-neutral-200 bg-white py-1 dark:border-neutral-700 dark:bg-neutral-800 z-[1000] rounded-lg border shadow-lg ${
        props.class || 'fixed'
      }`}
      style={props.style}
      onClick={(e) => e.stopPropagation()}
    >
      {props.children}
    </div>
  );
};

export interface MenuItemProps {
  label: string;
  icon?: any;
  onClick: () => void;
  variant?: 'danger' | 'default';
}

export const MenuItem: Component<MenuItemProps> = (props) => {
  return (
    <button
      class="w-full space-x-2 px-3 py-2 flex items-center text-left text-sm transition-colors"
      classList={{
        'hover:bg-neutral-50 dark:hover:bg-neutral-700': props.variant !== 'danger',
        'text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20': props.variant === 'danger',
      }}
      onClick={() => {
        props.onClick();
      }}
    >
      <Show when={props.icon} fallback={<div class="h-3 w-3" />}>
        <Icon path={props.icon} class="h-3 w-3" />
      </Show>
      <span>{props.label}</span>
    </button>
  );
};


================================================
FILE: packages/solid-repl/src/dockview/solid.tsx
================================================
import { GridviewPanel, SplitviewPanel } from 'dockview-core';
import { createRoot } from 'solid-js';
import { insert } from 'solid-js/web';

export class SolidPanelView extends SplitviewPanel {
  constructor(
    id: string,
    component: string,
    private readonly myComponent: any,
  ) {
    super(id, component);
  }
  getComponent() {
    const dispose = createRoot((dispose) => {
      insert(this.element, () => this.myComponent(this.params));
      return dispose;
    });
    return {
      update: () => {},
      dispose,
    };
  }
}

export class SolidGridPanelView extends GridviewPanel {
  constructor(
    id: string,
    component: string,
    private readonly myComponent: any,
  ) {
    super(id, component);
  }
  getComponent() {
    const dispose = createRoot((dispose) => {
      insert(this.element, () => this.myComponent.call(this, this.params));
      return dispose;
    });
    return {
      update: () => {},
      dispose,
    };
  }
}


================================================
FILE: packages/solid-repl/src/hooks/useZoom.ts
================================================
import { createStore, SetStoreFunction } from 'solid-js/store';

type ZoomState = {
  zoom: number;
  scaleIframe: boolean;
  overrideNative: boolean;
  fontSize: number;
  scale: number;
};

const getLS = () => {
  const result = localStorage.getItem('zoomState');
  if (result == null) return null;
  return JSON.parse(result) as ZoomState;
};

const ls = getLS();
const initFontSize = 14;
const initScale = 100;

const [zoomState, setZoomStateInternal] = createStore<ZoomState>({
  overrideNative: ls?.overrideNative || true,
  scaleIframe: ls?.scaleIframe || true,
  zoom: ls?.zoom || 100,
  get fontSize() {
    return initFontSize * (this.zoom / 100);
  },
  get scale() {
    return initScale * (100 / this.zoom);
  },
});

const setZoomState: SetStoreFunction<ZoomState> = (...args: any[]) => {
  (setZoomStateInternal as any)(...args);
  localStorage.setItem(
    'zoomState',
    JSON.stringify({
      zoom: zoomState.zoom,
      scaleIframe: zoomState.scaleIframe,
      overrideNative: zoomState.overrideNative,
    }),
  );
};

export const useZoom = () => {
  const updateZoom = (input: 'increase' | 'decrease' | 'reset') => {
    let { zoom } = zoomState;

    switch (input) {
      case 'increase':
        zoom += 10;
        break;
      case 'decrease':
        zoom -= 10;
        break;
      default:
        zoom = 100;
        break;
    }

    setZoomState('zoom', Math.min(Math.max(zoom, 40), 200));
  };

  return { zoomState, updateZoom, setZoomState };
};


================================================
FILE: packages/solid-repl/src/index.ts
================================================
import type { Tab } from 'solid-repl';

const indexTSX = `import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(1);
  const increment = () => setCount(count => count + 1);

  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app")!);
`;

export const defaultTabs: Tab[] = [
  {
    name: 'main.tsx',
    source: indexTSX,
  },
];


================================================
FILE: packages/solid-repl/src/repl.tsx
================================================
import { Repl } from './components/repl';
export default Repl;


================================================
FILE: packages/solid-repl/src/types.d.ts
================================================
declare module 'solid-repl' {
  export interface Tab {
    name: string;
    source: string;
  }

  export const defaultTabs: Tab[];
}

declare module 'solid-repl/dist/repl' {
  export type Repl = import('solid-js').Component<{
    compiler: Worker;
    formatter: Worker;
    linter: Worker;
    isHorizontal: boolean;
    dark: boolean;
    tabs: Tab[];
    id: string;
    hideDevtools?: boolean;
    setTabs: (tab: Tab[]) => void;
    reset: () => void;
    current: string | undefined;
    setCurrent: (tabId: string) => void;
    onUserEdit?: () => void;
    onEditorReady?: (
      editor: import('monaco-editor').editor.IStandaloneCodeEditor,
      monaco: {
        Uri: typeof import('monaco-editor').Uri;
        editor: typeof import('monaco-editor').editor;
      },
    ) => void;
  }>;
  const Repl: Repl;
  export default Repl;
}

interface Window {
  MonacoEnvironment: {
    getWorker: (_moduleId: unknown, label: string) => Worker;
  };
}


================================================
FILE: packages/solid-repl/tsconfig.build.json
================================================
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "noEmit": false
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules/"]
}


================================================
FILE: packages/solid-repl/tsconfig.json
================================================
{
  "extends": "../../tsconfig.json",
  "include": ["./src/**/*", "repl/**/*"],
  "exclude": ["node_modules/"]
}


================================================
FILE: packages/solid-repl/unocss.config.ts
================================================
import { theme } from '@unocss/preset-wind3';
import { defineConfig } from 'unocss';
import sharedConfig from '../../uno.config.ts';

export default defineConfig({
  ...(sharedConfig as any),
  theme: {
    ...(sharedConfig as any).theme,
    fontFamily: {
      sans: 'Gordita, ' + theme.fontFamily!.sans,
    },
  },
});


================================================
FILE: patches/monaco-editor.patch
================================================
diff --git a/esm/vs/editor/editor.main.js b/esm/vs/editor/editor.main.js
index 626b49d8529e1784a898a72811e199a385ba4440..55d691dc1659f0dde07c208259c5e6301d980701 100644
--- a/esm/vs/editor/editor.main.js
+++ b/esm/vs/editor/editor.main.js
@@ -2,87 +2,10 @@ import * as monaco_contribution from '../language/css/monaco.contribution.js';
 import * as monaco_contribution$1 from '../language/html/monaco.contribution.js';
 import * as monaco_contribution$2 from '../language/json/monaco.contribution.js';
 import * as monaco_contribution$3 from '../language/typescript/monaco.contribution.js';
-import '../basic-languages/abap/abap.contribution.js';
-import '../basic-languages/apex/apex.contribution.js';
-import '../basic-languages/azcli/azcli.contribution.js';
-import '../basic-languages/bat/bat.contribution.js';
-import '../basic-languages/bicep/bicep.contribution.js';
-import '../basic-languages/cameligo/cameligo.contribution.js';
-import '../basic-languages/clojure/clojure.contribution.js';
-import '../basic-languages/coffee/coffee.contribution.js';
-import '../basic-languages/cpp/cpp.contribution.js';
-import '../basic-languages/csharp/csharp.contribution.js';
-import '../basic-languages/csp/csp.contribution.js';
 import '../basic-languages/css/css.contribution.js';
-import '../basic-languages/cypher/cypher.contribution.js';
-import '../basic-languages/dart/dart.contribution.js';
-import '../basic-languages/dockerfile/dockerfile.contribution.js';
-import '../basic-languages/ecl/ecl.contribution.js';
-import '../basic-languages/elixir/elixir.contribution.js';
-import '../basic-languages/flow9/flow9.contribution.js';
-import '../basic-languages/fsharp/fsharp.contribution.js';
-import '../basic-languages/freemarker2/freemarker2.contribution.js';
-import '../basic-languages/go/go.contribution.js';
-import '../basic-languages/graphql/graphql.contribution.js';
-import '../basic-languages/handlebars/handlebars.contribution.js';
-import '../basic-languages/hcl/hcl.contribution.js';
 import '../basic-languages/html/html.contribution.js';
-import '../basic-languages/ini/ini.contribution.js';
-import '../basic-languages/java/java.contribution.js';
 import '../basic-languages/javascript/javascript.contribution.js';
-import '../basic-languages/julia/julia.contribution.js';
-import '../basic-languages/kotlin/kotlin.contribution.js';
-import '../basic-languages/less/less.contribution.js';
-import '../basic-languages/lexon/lexon.contribution.js';
-import '../basic-languages/lua/lua.contribution.js';
-import '../basic-languages/liquid/liquid.contribution.js';
-import '../basic-languages/m3/m3.contribution.js';
-import '../basic-languages/markdown/markdown.contribution.js';
-import '../basic-languages/mdx/mdx.contribution.js';
-import '../basic-languages/mips/mips.contribution.js';
-import '../basic-languages/msdax/msdax.contribution.js';
-import '../basic-languages/mysql/mysql.contribution.js';
-import '../basic-languages/objective-c/objective-c.contribution.js';
-import '../basic-languages/pascal/pascal.contribution.js';
-import '../basic-languages/pascaligo/pascaligo.contribution.js';
-import '../basic-languages/perl/perl.contribution.js';
-import '../basic-languages/pgsql/pgsql.contribution.js';
-import '../basic-languages/php/php.contribution.js';
-import '../basic-languages/pla/pla.contribution.js';
-import '../basic-languages/postiats/postiats.contribution.js';
-import '../basic-languages/powerquery/powerquery.contribution.js';
-import '../basic-languages/powershell/powershell.contribution.js';
-import '../basic-languages/protobuf/protobuf.contribution.js';
-import '../basic-languages/pug/pug.contribution.js';
-import '../basic-languages/python/python.contribution.js';
-import '../basic-languages/qsharp/qsharp.contribution.js';
-import '../basic-languages/r/r.contribution.js';
-import '../basic-languages/razor/razor.contribution.js';
-import '../basic-languages/redis/redis.contribution.js';
-import '../basic-languages/redshift/redshift.contribution.js';
-import '../basic-languages/restructuredtext/restructuredtext.contribution.js';
-import '../basic-languages/ruby/ruby.contribution.js';
-import '../basic-languages/rust/rust.contribution.js';
-import '../basic-languages/sb/sb.contribution.js';
-import '../basic-languages/scala/scala.contribution.js';
-import '../basic-languages/scheme/scheme.contribution.js';
-import '../basic-languages/scss/scss.contribution.js';
-import '../basic-languages/shell/shell.contribution.js';
-import '../basic-languages/solidity/solidity.contribution.js';
-import '../basic-languages/sophia/sophia.contribution.js';
-import '../basic-languages/sparql/sparql.contribution.js';
-import '../basic-languages/sql/sql.contribution.js';
-import '../basic-languages/st/st.contribution.js';
-import '../basic-languages/swift/swift.contribution.js';
-import '../basic-languages/systemverilog/systemverilog.contribution.js';
-import '../basic-languages/tcl/tcl.contribution.js';
-import '../basic-languages/twig/twig.contribution.js';
 import '../basic-languages/typescript/typescript.contribution.js';
-import '../basic-languages/typespec/typespec.contribution.js';
-import '../basic-languages/vb/vb.contribution.js';
-import '../basic-languages/wgsl/wgsl.contribution.js';
-import '../basic-languages/xml/xml.contribution.js';
-import '../basic-languages/yaml/yaml.contribution.js';
 import * as index from '../../external/monaco-lsp-client/out/index.js';
 export { index as lsp };
 import './browser/coreCommands.js';


================================================
FILE: pnpm-workspace.yaml
================================================
packages:
  - packages/*


================================================
FILE: scripts/oxlint-plugin-unocss.ts
================================================
import { createSyncFn } from 'synckit';
import { definePlugin, defineRule, type Ranged } from '@oxlint/plugins';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const validateClass = createSyncFn(resolve(__dirname, './unocss-worker.ts'));

const metadataCache = new Map<string, any>();

function getMetadata(cls: string) {
  if (metadataCache.has(cls)) return metadataCache.get(cls);
  const meta = validateClass(cls);
  metadataCache.set(cls, meta);
  return meta;
}

function getClassesFromAttribute(node: any): { cls: string; node: Ranged; definitelyActive: boolean }[] {
  const classes: { cls: string; node: Ranged; definitelyActive: boolean }[] = [];
  if (node.name.name === 'class' || node.name.name === 'classList') {
    if (node.value && node.value.type === 'Literal' && typeof node.value.value === 'string') {
      node.value.value
        .split(/\s+/)
        .filter(Boolean)
        .forEach((cls: string) => {
          classes.push({ cls, node: node.value, definitelyActive: true });
        });
    } else if (node.value && node.value.type === 'JSXExpressionContainer') {
      const expression = node.value.expression;
      if (expression && expression.type === 'ObjectExpression') {
        const props = expression.properties || [];
        for (const prop of props) {
          if (prop.type === 'Property') {
            const isDefinitelyActive = prop.value.type === 'Literal' && prop.value.value === true;
            const key = prop.key;
            if (key.type === 'Literal' && typeof key.value === 'string') {
              key.value
                .split(/\s+/)
                .filter(Boolean)
                .forEach((cls: string) => {
                  classes.push({ cls, node: key, definitelyActive: isDefinitelyActive });
                });
            } else if (key.type === 'Identifier') {
              classes.push({ cls: key.name, node: key, definitelyActive: isDefinitelyActive });
            }
          }
        }
      }
    }
  }
  return classes;
}

const validClassRule = defineRule({
  meta: {
    type: 'problem',
    docs: {
      description: 'Ensure UnoCSS classes are valid',
      category: 'Possible Errors',
    },
    messages: {
      invalidClass: 'Invalid UnoCSS class: {{cls}}',
    },
  },
  create(context) {
    return {
      JSXAttribute(node) {
        const classes = getClassesFromAttribute(node);
        for (const { cls, node: targetNode } of classes) {
          if (cls.startsWith('{') || cls.includes('$') || cls.includes('(') || cls === 'group') continue;
          const meta = getMetadata(cls);
          if (!meta.valid) {
            context.report({
              message: `Invalid UnoCSS class: ${cls}`,
              node: targetNode,
            });
          }
        }
      },
    };
  },
});

const conflictingClassesRule = defineRule({
  meta: {
    type: 'problem',
    docs: {
      description: 'Ensure UnoCSS classes do not conflict',
      category: 'Possible Errors',
    },
    messages: {
      conflictingClass: 'Conflicting UnoCSS class: {{cls}} and {{conflictingCls}} both set {{prop}}',
    },
  },
  create(context) {
    return {
      JSXAttribute(node) {
        const classes = getClassesFromAttribute(node);
        // We only check for conflicts among classes that are definitely active together
        const activeClasses = classes.filter((c) => c.definitelyActive);

        // context -> property -> class
        const propertyMap = new Map<string, Map<string, string>>();

        for (const { cls } of activeClasses) {
          if (cls.startsWith('{') || cls.includes('$') || cls.includes('(') || cls === 'group') continue;
          const meta = getMetadata(cls);
          if (!meta.valid || !meta.info) continue;

          for (const info of meta.info) {
            const clsContext = info.context || 'default';
            if (!propertyMap.has(clsContext)) {
              propertyMap.set(clsContext, new Map());
            }

            const propMap = propertyMap.get(clsContext)!;
            for (const prop of info.properties) {
              if (propMap.has(prop)) {
                const conflictingCls = propMap.get(prop)!;
                if (conflictingCls !== cls) {
                  context.report({
                    message: `Conflicting UnoCSS classes: "${cls}" and "${conflictingCls}" both set "${prop}" in context "${clsContext}"`,
                    node: node.value || node,
                  });
                }
              } else {
                propMap.set(prop, cls);
              }
            }
          }
        }
      },
    };
  },
});

const plugin = definePlugin({
  meta: { name: 'unocss' },
  rules: {
    'valid-class': validClassRule,
    'no-conflicting-classes': conflictingClassesRule,
  },
});

export default plugin;


================================================
FILE: scripts/unocss-worker.ts
================================================
import { runAsWorker } from 'synckit';
import { createGenerator } from 'unocss';
import { createJiti } from 'jiti';

const jiti = createJiti(import.meta.url);
const unoConfig = await jiti.import('../uno.config.ts');
const uno = await createGenerator((unoConfig as any).default);

runAsWorker(async (cls: string) => {
  const res = await uno.generate(cls, { preflights: false });
  if (res.matched.size === 0) return { valid: false };

  const info: { properties: string[]; context: string }[] = [];

  // Basic CSS parser to extract properties and their context
  // We expect simple CSS from uno.generate(single_class)
  const css = res.css;
  const stack: string[] = [];
  let currentContext = '';

  // Simple regex-based parsing
  // This is not a full CSS parser but should be enough for UnoCSS output for a single class
  const lines = css.split('\n');
  let currentProperties: string[] = [];
  let insideRule = false;

  for (let line of lines) {
    line = line.trim();
    if (!line || line.startsWith('/*')) continue;

    if (line.startsWith('@')) {
      const braceIndex = line.indexOf('{');
      const atRule = braceIndex !== -1 ? line.slice(0, braceIndex).trim() : line;
      stack.push(atRule);
      if (line.includes('}')) {
        // Single line at-rule? (rare in UnoCSS but possible)
        stack.pop();
      }
      continue;
    }

    if (line.includes('{')) {
      const selector = line.split('{')[0].trim();
      const escapedClsForRegex = cls.replace(/[:[\]()%/$,!]/g, '\\\\$&');
      const contextSelector = selector.replace(new RegExp(`\\.${escapedClsForRegex}`, 'g'), '').trim();

      currentContext = [...stack, contextSelector].filter(Boolean).join(' ');
      currentProperties = [];
      insideRule = true;

      if (line.includes('}')) {
        const content = line.slice(line.indexOf('{') + 1, line.lastIndexOf('}'));
        extractProperties(content, currentProperties);
        info.push({ properties: currentProperties, context: currentContext });
        currentProperties = [];
        insideRule = false;
      }
      continue;
    }

    if (line.includes('}')) {
      if (insideRule) {
        if (currentProperties.length > 0) {
          info.push({ properties: currentProperties, context: currentContext });
          currentProperties = [];
        }
        insideRule = false;
      } else {
        stack.pop();
      }
      continue;
    }

    if (insideRule && line.includes(':')) {
      extractProperties(line, currentProperties);
    }
  }

  return { valid: true, info };
});

function extractProperties(content: string, target: string[]) {
  const decls = content.split(';');
  for (const decl of decls) {
    const colonIndex = decl.indexOf(':');
    if (colonIndex !== -1) {
      const prop = decl.slice(0, colonIndex).trim();
      if (prop && !prop.startsWith('--un-')) {
        target.push(prop);
      }
    }
  }
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "allowSyntheticDefaultImports": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "moduleResolution": "bundler",
    "jsx": "preserve",
    "resolveJsonModule": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "skipLibCheck": true,
    "jsxImportSource": "solid-js",
    "allowImportingTsExtensions": true,
    "noEmit": true
  }
}


================================================
FILE: uno.config.ts
================================================
import { presetWind } from '@unocss/preset-wind3';
import { transformerDirectives, defineConfig } from 'unocss';

export default defineConfig({
  theme: {
    colors: {
      dark: '#07254A',
      medium: '#446b9e',
      solidc: '#2c4f7c',
    },
  },
  presets: [presetWind()],
  transformers: [transformerDirectives()],
});
Download .txt
gitextract_mlfkxwgk/

├── .gitignore
├── .oxfmtrc.json
├── .oxlintrc.json
├── LICENSE
├── README.md
├── package.json
├── packages/
│   ├── playground/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public/
│   │   │   ├── _redirects
│   │   │   ├── manifest.webmanifest
│   │   │   ├── robots.txt
│   │   │   └── sw.js
│   │   ├── src/
│   │   │   ├── app.tsx
│   │   │   ├── components/
│   │   │   │   ├── header.tsx
│   │   │   │   ├── setupSolid.ts
│   │   │   │   ├── update.tsx
│   │   │   │   └── zoomDropdown.tsx
│   │   │   ├── context.tsx
│   │   │   ├── index.tsx
│   │   │   ├── pages/
│   │   │   │   ├── edit.tsx
│   │   │   │   ├── home.tsx
│   │   │   │   └── login.tsx
│   │   │   └── utils/
│   │   │       ├── date.ts
│   │   │       ├── exportFiles.tsx
│   │   │       ├── isDarkTheme.ts
│   │   │       └── serviceWorker.ts
│   │   ├── tsconfig.json
│   │   ├── unocss.config.ts
│   │   └── vite.config.ts
│   └── solid-repl/
│       ├── build.ts
│       ├── package.json
│       ├── repl/
│       │   ├── compiler.ts
│       │   ├── formatter.ts
│       │   ├── linter.ts
│       │   └── main.css
│       ├── src/
│       │   ├── components/
│       │   │   ├── CompileMode.tsx
│       │   │   ├── editor/
│       │   │   │   ├── index.tsx
│       │   │   │   ├── monacoTabs.tsx
│       │   │   │   └── setupSolid.ts
│       │   │   ├── error.tsx
│       │   │   ├── newTab.tsx
│       │   │   ├── preview.tsx
│       │   │   ├── repl.tsx
│       │   │   └── ui/
│       │   │       ├── Button.tsx
│       │   │       ├── Checkbox.tsx
│       │   │       ├── IconButton.tsx
│       │   │       ├── Input.tsx
│       │   │       ├── Label.tsx
│       │   │       └── Menu.tsx
│       │   ├── dockview/
│       │   │   └── solid.tsx
│       │   ├── hooks/
│       │   │   └── useZoom.ts
│       │   ├── index.ts
│       │   ├── repl.tsx
│       │   └── types.d.ts
│       ├── tsconfig.build.json
│       ├── tsconfig.json
│       └── unocss.config.ts
├── patches/
│   └── monaco-editor.patch
├── pnpm-workspace.yaml
├── scripts/
│   ├── oxlint-plugin-unocss.ts
│   └── unocss-worker.ts
├── tsconfig.json
└── uno.config.ts
Download .txt
SYMBOL INDEX (64 symbols across 26 files)

FILE: packages/playground/public/sw.js
  function notifyClient (line 3) | async function notifyClient(event) {
  function responsesDiffer (line 13) | function responsesDiffer(cached, fresh) {
  function fetchAndCache (line 20) | async function fetchAndCache(cache, event) {
  function fetchWithCache (line 36) | async function fetchWithCache(event) {
  function handleFetch (line 51) | function handleFetch(event) {

FILE: packages/playground/src/components/header.tsx
  function shareLink (line 27) | function shareLink() {
  function closeMobileMenu (line 41) | function closeMobileMenu() {

FILE: packages/playground/src/context.tsx
  type AppContextType (line 5) | interface AppContextType {
  constant API (line 19) | const API = 'https://api.solidjs.com';
  method token (line 48) | get token() {
  method token (line 51) | set token(x) {
  method tabs (line 57) | tabs() {
  method setTabs (line 62) | setTabs(x) {
  method toggleDark (line 66) | toggleDark() {

FILE: packages/playground/src/pages/edit.tsx
  function parseHash (line 19) | function parseHash<T>(hash: string, fallback: T): T {
  method getWorker (line 30) | getWorker(_moduleId: unknown, label: string) {
  type InternalTab (line 45) | interface InternalTab extends Tab {
  method name (line 86) | get name() {
  method name (line 89) | set name(name: string) {
  method source (line 94) | get source() {
  method source (line 97) | set source(source: string) {

FILE: packages/playground/src/pages/home.tsx
  type ReplFile (line 11) | interface ReplFile {
  type APIRepl (line 15) | interface APIRepl {
  type Repls (line 26) | interface Repls {

FILE: packages/playground/src/utils/exportFiles.tsx
  function packageJSON (line 58) | function packageJSON(imports: string[]): string {
  function exportToZip (line 89) | async function exportToZip(tabs: Tab[]): Promise<void> {

FILE: packages/playground/src/utils/serviceWorker.ts
  function registerServiceWorker (line 6) | function registerServiceWorker(): void {

FILE: packages/solid-repl/repl/compiler.ts
  function uid (line 9) | function uid(str: string) {
  function babelTransform (line 15) | function babelTransform(filename: string, code: string, externals: Recor...
  function transformTab (line 57) | function transformTab(tab: Tab, externals: Record<string, string>): stri...
  function compile (line 77) | function compile(tabs: Tab[], event: string) {
  function babel (line 87) | function babel(tab: Tab, compileOpts: any) {

FILE: packages/solid-repl/repl/formatter.ts
  function format (line 5) | function format(code: string) {

FILE: packages/solid-repl/repl/linter.ts
  type LinterWorkerPayload (line 5) | interface LinterWorkerPayload {

FILE: packages/solid-repl/src/components/CompileMode.tsx
  type CompileModeProps (line 16) | interface CompileModeProps {

FILE: packages/solid-repl/src/components/editor/index.tsx
  method provideDocumentFormattingEdits (line 35) | async provideDocumentFormattingEdits(model) {

FILE: packages/solid-repl/src/components/editor/setupSolid.ts
  function register (line 38) | function register() {

FILE: packages/solid-repl/src/components/newTab.tsx
  type NewTabProps (line 20) | interface NewTabProps {

FILE: packages/solid-repl/src/components/preview.tsx
  type Props (line 353) | type Props = {

FILE: packages/solid-repl/src/components/repl.tsx
  method createComponent (line 405) | createComponent(options) {

FILE: packages/solid-repl/src/components/ui/Button.tsx
  type ButtonProps (line 11) | interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
  type LinkButtonProps (line 33) | interface LinkButtonProps extends JSX.AnchorHTMLAttributes<HTMLAnchorEle...

FILE: packages/solid-repl/src/components/ui/Checkbox.tsx
  type CheckboxProps (line 3) | interface CheckboxProps extends JSX.InputHTMLAttributes<HTMLInputElement> {

FILE: packages/solid-repl/src/components/ui/IconButton.tsx
  type IconButtonProps (line 4) | interface IconButtonProps {

FILE: packages/solid-repl/src/components/ui/Input.tsx
  type InputProps (line 3) | interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {

FILE: packages/solid-repl/src/components/ui/Menu.tsx
  type MenuProps (line 4) | interface MenuProps {
  type MenuItemProps (line 24) | interface MenuItemProps {

FILE: packages/solid-repl/src/dockview/solid.tsx
  class SolidPanelView (line 5) | class SolidPanelView extends SplitviewPanel {
    method constructor (line 6) | constructor(
    method getComponent (line 13) | getComponent() {
  class SolidGridPanelView (line 25) | class SolidGridPanelView extends GridviewPanel {
    method constructor (line 26) | constructor(
    method getComponent (line 33) | getComponent() {

FILE: packages/solid-repl/src/hooks/useZoom.ts
  type ZoomState (line 3) | type ZoomState = {
  method fontSize (line 25) | get fontSize() {
  method scale (line 28) | get scale() {

FILE: packages/solid-repl/src/types.d.ts
  type Tab (line 2) | interface Tab {
  type Repl (line 11) | type Repl = import('solid-js').Component
  type Window (line 37) | interface Window {

FILE: scripts/oxlint-plugin-unocss.ts
  function getMetadata (line 13) | function getMetadata(cls: string) {
  function getClassesFromAttribute (line 20) | function getClassesFromAttribute(node: any): { cls: string; node: Ranged...
  method create (line 67) | create(context) {
  method create (line 97) | create(context) {

FILE: scripts/unocss-worker.ts
  function extractProperties (line 82) | function extractProperties(content: string, target: string[]) {
Condensed preview — 63 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (156K chars).
[
  {
    "path": ".gitignore",
    "chars": 30,
    "preview": "node_modules/\ndist/\n.DS_Store\n"
  },
  {
    "path": ".oxfmtrc.json",
    "chars": 411,
    "preview": "{\n  \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n  \"arrowParens\": \"always\",\n  \"htmlWhitespaceSensitivity"
  },
  {
    "path": ".oxlintrc.json",
    "chars": 195,
    "preview": "{\n  \"jsPlugins\": [\"./scripts/oxlint-plugin-unocss.ts\"],\n  \"rules\": {\n    \"unocss/valid-class\": \"error\",\n    \"unocss/no-c"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2022 SolidJS Core Team\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 1795,
    "preview": "<p>\n  <img width=\"100%\" src=\"https://assets.solidjs.com/banner?project=Playground&type=core\" alt=\"Solid Playground\">\n</p"
  },
  {
    "path": "package.json",
    "chars": 824,
    "preview": "{\n  \"name\": \"solid-playground-restructured\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"\",\n  \"keywords\":"
  },
  {
    "path": "packages/playground/index.html",
    "chars": 613,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "packages/playground/package.json",
    "chars": 1015,
    "preview": "{\n  \"name\": \"solid-playground\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"st"
  },
  {
    "path": "packages/playground/public/_redirects",
    "chars": 24,
    "preview": "/*    /index.html   200\n"
  },
  {
    "path": "packages/playground/public/manifest.webmanifest",
    "chars": 431,
    "preview": "{\n  \"dir\": \"ltr\",\n  \"lang\": \"en\",\n  \"name\": \"Solid REPL\",\n  \"short_name\": \"Solid REPL\",\n  \"scope\": \"/\",\n  \"display\": \"st"
  },
  {
    "path": "packages/playground/public/robots.txt",
    "chars": 22,
    "preview": "User-agent: *\nAllow: /"
  },
  {
    "path": "packages/playground/public/sw.js",
    "chars": 1981,
    "preview": "const cacheName = 'my-cache';\n\nasync function notifyClient(event) {\n  const client = event.clientId ? await clients.get("
  },
  {
    "path": "packages/playground/src/app.tsx",
    "chars": 1668,
    "preview": "import { Show, JSX, Suspense } from 'solid-js';\nimport { Route, Router } from '@solidjs/router';\nimport { eventBus, setE"
  },
  {
    "path": "packages/playground/src/components/header.tsx",
    "chars": 7612,
    "preview": "import Dismiss from 'solid-dismiss';\nimport { A } from '@solidjs/router';\nimport { Icon } from 'solid-heroicons';\nimport"
  },
  {
    "path": "packages/playground/src/components/setupSolid.ts",
    "chars": 477,
    "preview": "import { typescript } from 'monaco-editor';\n\nconst solidTypes: Record<string, string> = import.meta.glob('/node_modules/"
  },
  {
    "path": "packages/playground/src/components/update.tsx",
    "chars": 1254,
    "preview": "import type { Component } from 'solid-js';\nimport { Portal } from 'solid-js/web';\n\nimport { Icon } from 'solid-heroicons"
  },
  {
    "path": "packages/playground/src/components/zoomDropdown.tsx",
    "chars": 5084,
    "preview": "import { Icon } from 'solid-heroicons';\nimport { magnifyingGlassPlus, minus, plus } from 'solid-heroicons/outline';\nimpo"
  },
  {
    "path": "packages/playground/src/context.tsx",
    "chars": 2201,
    "preview": "import { Accessor, createContext, createResource, createSignal, ParentComponent, Resource, useContext } from 'solid-js';"
  },
  {
    "path": "packages/playground/src/index.tsx",
    "chars": 275,
    "preview": "import { render } from 'solid-js/web';\nimport { App } from './app';\nimport { registerServiceWorker } from './utils/servi"
  },
  {
    "path": "packages/playground/src/pages/edit.tsx",
    "chars": 11706,
    "preview": "import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';\nimport tsWorker from 'monaco-editor/esm/vs/"
  },
  {
    "path": "packages/playground/src/pages/home.tsx",
    "chars": 8145,
    "preview": "import { A, useParams } from '@solidjs/router';\nimport { Icon } from 'solid-heroicons';\nimport { eye, eyeSlash, plus, xM"
  },
  {
    "path": "packages/playground/src/pages/login.tsx",
    "chars": 287,
    "preview": "import { Navigate, useSearchParams } from '@solidjs/router';\nimport { useAppContext } from '../context';\n\nexport const L"
  },
  {
    "path": "packages/playground/src/utils/date.ts",
    "chars": 806,
    "preview": "const formatter = new Intl.RelativeTimeFormat('en');\nexport const timeAgo = (ms: number): string => {\n  const sec = Math"
  },
  {
    "path": "packages/playground/src/utils/exportFiles.tsx",
    "chars": 2779,
    "preview": "import pkg from '../../package.json';\nimport type { Tab } from 'solid-repl';\nimport dedent from 'dedent';\n\nconst viteCon"
  },
  {
    "path": "packages/playground/src/utils/isDarkTheme.ts",
    "chars": 447,
    "preview": "export const isDarkTheme = () => {\n  if (typeof window !== 'undefined') {\n    if (window.localStorage) {\n      const isD"
  },
  {
    "path": "packages/playground/src/utils/serviceWorker.ts",
    "chars": 633,
    "preview": "import { register } from 'register-service-worker';\nimport { createSignal } from 'solid-js';\n\nconst [eventBus, setEventB"
  },
  {
    "path": "packages/playground/tsconfig.json",
    "chars": 216,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\", \"WebWorker\"],\n"
  },
  {
    "path": "packages/playground/unocss.config.ts",
    "chars": 403,
    "preview": "import { theme } from '@unocss/preset-wind3';\nimport { defineConfig } from 'unocss';\nimport sharedConfig from '../../uno"
  },
  {
    "path": "packages/playground/vite.config.ts",
    "chars": 851,
    "preview": "import { defineConfig } from 'vite';\nimport solidPlugin from 'vite-plugin-solid';\nimport UnoCSS from 'unocss/vite';\n\nexp"
  },
  {
    "path": "packages/solid-repl/build.ts",
    "chars": 849,
    "preview": "import { build } from 'esbuild';\nimport { readFileSync, writeFileSync, unlinkSync } from 'fs';\nimport { copyFileSync } f"
  },
  {
    "path": "packages/solid-repl/package.json",
    "chars": 1727,
    "preview": "{\n  \"name\": \"solid-repl\",\n  \"version\": \"0.26.0\",\n  \"description\": \"Quickly discover what the solid compiler will generat"
  },
  {
    "path": "packages/solid-repl/repl/compiler.ts",
    "chars": 3331,
    "preview": "import type { Tab } from 'solid-repl';\n\nimport { transform } from '@babel/standalone';\n// @ts-ignore\nimport babelPresetS"
  },
  {
    "path": "packages/solid-repl/repl/formatter.ts",
    "chars": 608,
    "preview": "import { format as prettierFormat } from 'prettier/standalone';\nimport * as prettierPluginBabel from 'prettier/plugins/b"
  },
  {
    "path": "packages/solid-repl/repl/linter.ts",
    "chars": 1590,
    "preview": "import { verify, verifyAndFix } from 'eslint-solid-standalone';\nimport type { Linter } from 'eslint-solid-standalone';\ni"
  },
  {
    "path": "packages/solid-repl/repl/main.css",
    "chars": 1708,
    "preview": "@import url('@unocss/reset/tailwind.css');\n\n@font-face {\n  font-family: 'Gordita';\n  src: url('/Gordita-Regular.woff') f"
  },
  {
    "path": "packages/solid-repl/src/components/CompileMode.tsx",
    "chars": 2283,
    "preview": "import { Component, Setter } from 'solid-js';\nimport { Label } from './ui/Label';\nimport { Input } from './ui/Input';\n\ne"
  },
  {
    "path": "packages/solid-repl/src/components/editor/index.tsx",
    "chars": 5948,
    "preview": "import { Component, createEffect, onMount, onCleanup, Show } from 'solid-js';\nimport { Uri, languages, editor as mEditor"
  },
  {
    "path": "packages/solid-repl/src/components/editor/monacoTabs.tsx",
    "chars": 1343,
    "preview": "import { createMemo, onCleanup } from 'solid-js';\nimport type { Tab } from 'solid-repl';\nimport { Uri, editor, IDisposab"
  },
  {
    "path": "packages/solid-repl/src/components/editor/setupSolid.ts",
    "chars": 1746,
    "preview": "import { languages, editor, typescript } from 'monaco-editor';\nimport { shikiToMonaco } from '@shikijs/monaco';\nimport {"
  },
  {
    "path": "packages/solid-repl/src/components/error.tsx",
    "chars": 1420,
    "preview": "import { Component, createMemo, createSignal } from 'solid-js';\n\nimport { Icon } from 'solid-heroicons';\nimport { chevro"
  },
  {
    "path": "packages/solid-repl/src/components/newTab.tsx",
    "chars": 11275,
    "preview": "import { Component, createSignal, For, createMemo, onMount, Show } from 'solid-js';\nimport { Icon } from 'solid-heroicon"
  },
  {
    "path": "packages/solid-repl/src/components/preview.tsx",
    "chars": 12889,
    "preview": "import { Component, createEffect, JSX, onCleanup, onMount } from 'solid-js';\nimport { useZoom } from '../hooks/useZoom';"
  },
  {
    "path": "packages/solid-repl/src/components/repl.tsx",
    "chars": 21245,
    "preview": "import { createSignal, createEffect, batch, onCleanup, createMemo, onMount, Show, createRoot, JSX } from 'solid-js';\nimp"
  },
  {
    "path": "packages/solid-repl/src/components/ui/Button.tsx",
    "chars": 1751,
    "preview": "import { ParentComponent, JSX, splitProps } from 'solid-js';\n\nconst baseClasses =\n  'flex items-center rounded-md transi"
  },
  {
    "path": "packages/solid-repl/src/components/ui/Checkbox.tsx",
    "chars": 747,
    "preview": "import { Component, JSX, splitProps } from 'solid-js';\n\nexport interface CheckboxProps extends JSX.InputHTMLAttributes<H"
  },
  {
    "path": "packages/solid-repl/src/components/ui/IconButton.tsx",
    "chars": 1212,
    "preview": "import { Component, JSX } from 'solid-js';\nimport { Icon } from 'solid-heroicons';\n\nexport interface IconButtonProps {\n "
  },
  {
    "path": "packages/solid-repl/src/components/ui/Input.tsx",
    "chars": 744,
    "preview": "import { Component, JSX, splitProps } from 'solid-js';\n\nexport interface InputProps extends JSX.InputHTMLAttributes<HTML"
  },
  {
    "path": "packages/solid-repl/src/components/ui/Label.tsx",
    "chars": 396,
    "preview": "import { ParentComponent, JSX, splitProps } from 'solid-js';\n\nexport const Label: ParentComponent<JSX.LabelHTMLAttribute"
  },
  {
    "path": "packages/solid-repl/src/components/ui/Menu.tsx",
    "chars": 1344,
    "preview": "import { ParentComponent, Component, Show } from 'solid-js';\nimport { Icon } from 'solid-heroicons';\n\nexport interface M"
  },
  {
    "path": "packages/solid-repl/src/dockview/solid.tsx",
    "chars": 971,
    "preview": "import { GridviewPanel, SplitviewPanel } from 'dockview-core';\nimport { createRoot } from 'solid-js';\nimport { insert } "
  },
  {
    "path": "packages/solid-repl/src/hooks/useZoom.ts",
    "chars": 1487,
    "preview": "import { createStore, SetStoreFunction } from 'solid-js/store';\n\ntype ZoomState = {\n  zoom: number;\n  scaleIframe: boole"
  },
  {
    "path": "packages/solid-repl/src/index.ts",
    "chars": 514,
    "preview": "import type { Tab } from 'solid-repl';\n\nconst indexTSX = `import { render } from \"solid-js/web\";\nimport { createSignal }"
  },
  {
    "path": "packages/solid-repl/src/repl.tsx",
    "chars": 63,
    "preview": "import { Repl } from './components/repl';\nexport default Repl;\n"
  },
  {
    "path": "packages/solid-repl/src/types.d.ts",
    "chars": 958,
    "preview": "declare module 'solid-repl' {\n  export interface Tab {\n    name: string;\n    source: string;\n  }\n\n  export const default"
  },
  {
    "path": "packages/solid-repl/tsconfig.build.json",
    "chars": 192,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"noEmit\": f"
  },
  {
    "path": "packages/solid-repl/tsconfig.json",
    "chars": 113,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"./src/**/*\", \"repl/**/*\"],\n  \"exclude\": [\"node_modules/\"]\n}\n"
  },
  {
    "path": "packages/solid-repl/unocss.config.ts",
    "chars": 323,
    "preview": "import { theme } from '@unocss/preset-wind3';\nimport { defineConfig } from 'unocss';\nimport sharedConfig from '../../uno"
  },
  {
    "path": "patches/monaco-editor.patch",
    "chars": 5505,
    "preview": "diff --git a/esm/vs/editor/editor.main.js b/esm/vs/editor/editor.main.js\nindex 626b49d8529e1784a898a72811e199a385ba4440."
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 25,
    "preview": "packages:\n  - packages/*\n"
  },
  {
    "path": "scripts/oxlint-plugin-unocss.ts",
    "chars": 4938,
    "preview": "import { createSyncFn } from 'synckit';\nimport { definePlugin, defineRule, type Ranged } from '@oxlint/plugins';\nimport "
  },
  {
    "path": "scripts/unocss-worker.ts",
    "chars": 2899,
    "preview": "import { runAsWorker } from 'synckit';\nimport { createGenerator } from 'unocss';\nimport { createJiti } from 'jiti';\n\ncon"
  },
  {
    "path": "tsconfig.json",
    "chars": 514,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n "
  },
  {
    "path": "uno.config.ts",
    "chars": 328,
    "preview": "import { presetWind } from '@unocss/preset-wind3';\nimport { transformerDirectives, defineConfig } from 'unocss';\n\nexport"
  }
]

About this extraction

This page contains the full source code of the solidjs/solid-playground GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 63 files (142.8 KB), approximately 37.5k tokens, and a symbol index with 64 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!