Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
},
"devDependencies": {
"@codemirror/lang-python": "^6.2.1",
"@codemirror/view": "^6.43.1",
"@codemirror/theme-one-dark": "^6.1.3",
"@eslint/js": "^10.0.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^24",
"codemirror": "^6.0.2",
"eslint": "^10.4.1",
Expand All @@ -30,6 +31,7 @@
"pyodide": "^314.0.0",
"svelte": "^5.56.3",
"svelte-check": "^4.3.4",
"tailwindcss": "^4.3.3",
"typescript": "^5.9.3",
"typescript-eslint": "^8.60.1",
"vite": "^7.2.6",
Expand Down
1 change: 1 addition & 0 deletions src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
64 changes: 64 additions & 0 deletions src/lib/playground/Editor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import type { EditorView } from "codemirror";
import { onMount } from "svelte";

type Props = {
value: string;
onupdate: (value: string) => void;
onload: () => void;
};

let { value, onupdate, onload }: Props = $props();

let element: HTMLDivElement;
let view: EditorView | undefined;

let loading = $state(true);

const setup = async () => {
const [{ basicSetup, EditorView }, { python }, { oneDark }] =
await Promise.all([
import("codemirror"),
import("@codemirror/lang-python"),
import("@codemirror/theme-one-dark"),
]);

loading = false;

view = new EditorView({
doc: value,
extensions: [
basicSetup,
oneDark,
EditorView.theme({
"&": {
height: "100%",
fontSize: "14px",
},
}),
python(),
],
parent: element,

dispatchTransactions: (txs, view) => {
view.update(txs);
onupdate(view.state.doc.toString());
},
});

onload();
};

onMount(() => {
setup();
return () => {
view?.destroy();
};
});
</script>

<div class="h-full w-full" id="editor" bind:this={element}>
{#if loading}
Loading...
{/if}
</div>
35 changes: 35 additions & 0 deletions src/lib/playground/StatusBar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import type { Status } from "$lib/playground/lib/state";
import StatusItem from "$lib/playground/components/StatusItem.svelte";

type Props = { status: Status };

let { status }: Props = $props();
</script>

<div
class="min-h-6 max-h-6 px-2 flex justify-between items-center bg-gray-950 text-blue-50 text-xs font-medium"
>
<div>
<p>
{#if status.has_unsaved_changes}
Unsaved changes
{:else}
Up to date
{/if}
</p>
</div>

<div class="flex gap-2">
<StatusItem
type="Evaluate"
success={status.eval_success}
time={status.eval_time}
/>
<StatusItem
type="Check"
success={status.check_success}
time={status.check_time}
/>
</div>
</div>
20 changes: 20 additions & 0 deletions src/lib/playground/Terminal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { type Stdout } from "$lib/playground/lib/stdout.svelte";
type Props = { terminalHeight: number | undefined; stdout: Stdout };
let { terminalHeight, stdout }: Props = $props();

const MIN_HEIGHT = 128;

let height = $derived(terminalHeight ?? MIN_HEIGHT);
</script>

<div
class="min-h-32 font-mono p-2 overflow-y-scroll overflow-x-clip"
style="height: {height}px; min-height: {MIN_HEIGHT}px; max-height: {height}px;"
>
{#each $state.eager(stdout.lines) as line, index (index)}
<p class="whitespace-pre m-0 {line.is_err ? 'text-red-500' : 'text-white'}">
{line.content}
</p>
{/each}
</div>
73 changes: 73 additions & 0 deletions src/lib/playground/TerminalDragger.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script lang="ts">
type Props = { setHeight: (height: number) => void };
let { setHeight }: Props = $props();

let availableHeight: number | undefined = $state();

// when the dragger is clicked, we register an event listener for mouse movement,
// in order to extract the y position of the mouse. when the click is released
// (i.e. user has stopped click-and-dragging), we deregister the event listener.
//
// TODO: check mobile. there is a touchdown or similar event. pointer down might
// be more approrpriate and cover both.
//
const onmousedown = () => {
const onmousemove = ({ clientY }: MouseEvent) => {
if (availableHeight === undefined) {
return;
}

// the height of the window, offset by the mouse y position,
// minus the height of this dragger element itself, minus
// the statusbar height
const proposedHeight = availableHeight - clientY - 24;

// these aren't strictly necessary, but it will make it easier
// to not break anything here
const minHeight = 128;
const maxHeight = (availableHeight / 3) * 2;

const boundedHeight = Math.max(
minHeight,
Math.min(maxHeight, proposedHeight),
);

setHeight(boundedHeight);
};

const onmouseup = () => {
window.removeEventListener("mouseup", onmouseup);
window.removeEventListener("mousemove", onmousemove);
};

window.addEventListener("mouseup", onmouseup);
window.addEventListener("mousemove", onmousemove);
};
</script>

<svelte:window bind:innerHeight={availableHeight} />

<button
{onmousedown}
aria-label="Resize terminal"
class="h-px bg-gray-700 hover:bg-gray-600 active:bg-blue-900 flex justify-center items-center cursor-ns-resize group"
>
<div class=" z-10 w-full flex justify-center">
<div
class="size-5 text-blue-200 bg-gray-700/50 group-hover:bg-gray-500/50 group-active:bg-blue-900/50 rounded-full flex justify-center items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
</button>
33 changes: 33 additions & 0 deletions src/lib/playground/Toolbar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script>
import Kbd from "./components/Kbd.svelte";
import ToolbarButton from "./components/ToolbarButton.svelte";
import ToolbarLink from "./components/ToolbarLink.svelte";

let { check, run, is_busy } = $props();
</script>

<div class="flex h-10">
<ToolbarLink href="/">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M12.5 9.75A2.75 2.75 0 0 0 9.75 7H4.56l2.22 2.22a.75.75 0 1 1-1.06 1.06l-3.5-3.5a.75.75 0 0 1 0-1.06l3.5-3.5a.75.75 0 0 1 1.06 1.06L4.56 5.5h5.19a4.25 4.25 0 0 1 0 8.5h-1a.75.75 0 0 1 0-1.5h1a2.75 2.75 0 0 0 2.75-2.75Z"
clip-rule="evenodd"
/>
</svg>

<p>PurePy</p></ToolbarLink
>

<span class="ml-auto"></span>
<ToolbarButton onclick={check} busy={is_busy}>Check</ToolbarButton>
<ToolbarButton onclick={run} busy={is_busy}>
Run
<Kbd>⌘S</Kbd>
</ToolbarButton>
</div>
9 changes: 9 additions & 0 deletions src/lib/playground/components/Kbd.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
let { children } = $props();
</script>

<div
class=" border-gray-500 border px-1 rounded-sm text-xs font-light font-sans"
>
{@render children()}
</div>
46 changes: 46 additions & 0 deletions src/lib/playground/components/StatusItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
type Props = {
type: "Check" | "Evaluate";
success: boolean | null;
time: number;
};

let { type, success, time }: Props = $props();
</script>

<div class="flex items-center gap-1">
<div class="shrink-0">
{#if success === true}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4 text-green-500"
>
<path
fill-rule="evenodd"
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
{:else if success === false}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4 text-red-500"
>
<path
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
/>
</svg>
{/if}
</div>

{#if success !== null}
<p>
<span>{type}</span>
<span class="text-gray-400 font-light ml-0.5">{time}ms</span>
</p>
{/if}
</div>
16 changes: 16 additions & 0 deletions src/lib/playground/components/ToolbarButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import ToolbarItem from "./ToolbarItem.svelte";

let { children, onclick, busy = false } = $props();
</script>

<button
{onclick}
disabled={busy}
class="h-full {busy ? 'cursor-wait' : 'cursor-pointer'}"
type="button"
>
<ToolbarItem>
{@render children()}
</ToolbarItem>
</button>
9 changes: 9 additions & 0 deletions src/lib/playground/components/ToolbarItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
let { children } = $props();
</script>

<div
class="h-full inline-flex items-center gap-x-1.5 px-2.5 py-1 text-sm font-semibold text-white shadow-xs hover:bg-zinc-700"
>
{@render children()}
</div>
27 changes: 27 additions & 0 deletions src/lib/playground/components/ToolbarLink.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import ToolbarItem from "./ToolbarItem.svelte";

let { children, href, external = false } = $props();
</script>

<a class="h-full" target={external ? "_blank" : "_self"} {href}>
<ToolbarItem>
{@render children()}

{#if external}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4"
>
<path
d="M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z"
/>
<path
d="M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z"
/>
</svg>
{/if}
</ToolbarItem>
</a>
Loading