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
2,280 changes: 2,171 additions & 109 deletions playground/package-lock.json

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions playground/ruff/src/Editor/Chrome.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import ruffSchema from "../../../../ruff.schema.json";
import { useCallback, useMemo, useRef, useState } from "react";
import { Header, useTheme, setupMonaco } from "shared";
import { persist, persistLocal, restore, stringify } from "./settings";
import {
copyAsMarkdown,
copyAsMarkdownLink,
persist,
persistLocal,
restore,
stringify,
} from "./settings";
import { default as Editor, Source } from "./Editor";
import { loader } from "@monaco-editor/react";
import { DEFAULT_PYTHON_SOURCE } from "../constants";
Expand All @@ -23,6 +30,20 @@ export default function Chrome() {
await persist(settings, pythonSource);
}, [pythonSource, settings]);

const handleCopyMarkdownLink = useCallback(async () => {
if (settings == null || pythonSource == null) {
return;
}
await copyAsMarkdownLink(settings, pythonSource);
}, [pythonSource, settings]);

const handleCopyMarkdown = useCallback(async () => {
if (settings == null || pythonSource == null) {
return;
}
await copyAsMarkdown(settings, pythonSource);
}, [pythonSource, settings]);

if (initPromise.current == null) {
initPromise.current = startPlayground()
.then(({ sourceCode, settings, ruffVersion }) => {
Expand Down Expand Up @@ -81,12 +102,14 @@ export default function Chrome() {
return (
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
<Header
edit={revision}
theme={theme}
tool="ruff"
version={`v${ruffVersion}`}
onChangeTheme={setTheme}
edit={revision}
onShare={handleShare}
onCopyMarkdownLink={handleCopyMarkdownLink}
onCopyMarkdown={handleCopyMarkdown}
onReset={handleResetClicked}
/>

Expand Down
2 changes: 1 addition & 1 deletion playground/ruff/src/Editor/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const API_URL = import.meta.env.PROD
? "https://api.astral-1ad.workers.dev"
: "http://0.0.0.0:8787";
: "http://localhost:8787";

export type Playground = {
pythonSource: string;
Expand Down
59 changes: 56 additions & 3 deletions playground/ruff/src/Editor/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,67 @@ export function stringify(settings: Settings): string {
}

/**
* Persist the configuration to a URL.
* Save the configuration and return a shareable URL.
*/
async function shareUrl(
settingsSource: string,
pythonSource: string,
): Promise<string> {
const id = await savePlayground({ settingsSource, pythonSource });
return `${window.location.origin}/${encodeURIComponent(id)}`;
}

/**
* Persist the configuration and copy a shareable URL to clipboard.
*/
export async function persist(
settingsSource: string,
pythonSource: string,
): Promise<void> {
const id = await savePlayground({ settingsSource, pythonSource });
await navigator.clipboard.writeText(`${window.location.origin}/${id}`);
const url = await shareUrl(settingsSource, pythonSource);
await navigator.clipboard.writeText(url);
}

/**
* Persist the configuration and copy a markdown link to clipboard.
*/
export async function copyAsMarkdownLink(
settingsSource: string,
pythonSource: string,
): Promise<void> {
const url = await shareUrl(settingsSource, pythonSource);
await navigator.clipboard.writeText(`[Playground](${url})`);
}

/**
* Persist the configuration and copy markdown with code to clipboard.
*/
export async function copyAsMarkdown(
settingsSource: string,
pythonSource: string,
): Promise<void> {
const [url, toml] = await Promise.all([
shareUrl(settingsSource, pythonSource),
import("smol-toml"),
]);

let settingsBlock: string;
try {
settingsBlock = `\`\`\`toml\n${toml.stringify(JSON.parse(settingsSource))}\n\`\`\``;
} catch {
settingsBlock = `\`\`\`json\n${settingsSource}\n\`\`\``;
}

await navigator.clipboard.writeText(
`## [Playground](${url})

\`\`\`py
${pythonSource}
\`\`\`

${settingsBlock}
`,
);
}

/**
Expand Down
1 change: 1 addition & 0 deletions playground/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"classnames": "^2.3.2",
"react-aria-components": "^1.16.0",
"react": "^19.0.0",
"react-resizable-panels": "^4.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion playground/shared/src/AstralButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function AstralButton({
className,
children,
...otherProps
}: ButtonHTMLAttributes<any>) {
}: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
className={classNames(
Expand Down
17 changes: 13 additions & 4 deletions playground/shared/src/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import classNames from "classnames";
import RepoButton from "./RepoButton";
import ThemeButton from "./ThemeButton";
import ShareButton from "./ShareButton";
import ThemeButton from "./ThemeButton";
import { Theme } from "./theme";
import VersionTag from "./VersionTag";
import AstralButton from "./AstralButton";

export default function Header({
edit,
theme,
tool,
version,
onChangeTheme,
onReset,
edit,
onShare,
onCopyMarkdownLink,
onCopyMarkdown,
}: {
edit: number | null;
theme: Theme;
tool: "ruff" | "ty";
version: string | null;
onChangeTheme: (theme: Theme) => void;
onReset?(): void;
edit: number;
onShare: () => Promise<void>;
onCopyMarkdownLink: () => Promise<void>;
onCopyMarkdown: () => Promise<void>;
}) {
return (
<div
Expand Down Expand Up @@ -52,7 +56,12 @@ export default function Header({
<ResetButton onClicked={onReset} />
</div>
<div className="max-sm:hidden flex">
<ShareButton key={edit} onShare={onShare} />
<ShareButton
key={edit}
onShare={onShare}
onCopyMarkdownLink={onCopyMarkdownLink}
onCopyMarkdown={onCopyMarkdown}
/>
</div>
<Divider />

Expand Down
151 changes: 106 additions & 45 deletions playground/shared/src/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,122 @@
import { useEffect, useState } from "react";
import classNames from "classnames";
import { startTransition, useActionState, useEffect } from "react";
import {
Menu,
MenuItem as AriaMenuItem,
type MenuItemProps,
MenuTrigger,
Popover,
Pressable,
} from "react-aria-components";
import AstralButton from "./AstralButton";

type ShareStatus = "initial" | "copying" | "copied";
type ShareStatus = "initial" | "copied";
type ShareAction = "share" | "copyMarkdownLink" | "copyMarkdown" | "reset";

export default function ShareButton({
onShare,
onCopyMarkdownLink,
onCopyMarkdown,
}: {
onShare: () => Promise<void>;
onCopyMarkdownLink: () => Promise<void>;
onCopyMarkdown: () => Promise<void>;
}) {
const [status, setStatus] = useState<ShareStatus>("initial");
const [status, dispatch, isPending] = useActionState(
async (_previousStatus: ShareStatus, action: ShareAction) => {
switch (action) {
case "reset":
return "initial";
case "share":
await onShare();
break;
case "copyMarkdownLink":
await onCopyMarkdownLink();
break;
case "copyMarkdown":
await onCopyMarkdown();
break;
}
return "copied";
},
"initial",
);

useEffect(() => {
if (status === "copied") {
const timeout = setTimeout(() => setStatus("initial"), 2000);
const timeout = setTimeout(
() => startTransition(() => dispatch("reset")),
2000,
);
return () => clearTimeout(timeout);
}
}, [status]);
}, [status, dispatch]);

return status === "copied" ? (
<AstralButton
type="button"
className="relative flex-none leading-6 py-1.5 px-3 cursor-auto dark:shadow-copied"
>
<span
className="absolute inset-0 flex items-center justify-center invisible"
aria-hidden="true"
>
Share
</span>
<span aria-hidden="false">Copied!</span>
</AstralButton>
) : (
<AstralButton
type="button"
className="relative flex-none leading-6 py-1.5 px-3 shadow-xs disabled:opacity-50"
disabled={status === "copying"}
onClick={async () => {
setStatus("copying");
try {
await onShare();
setStatus("copied");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Failed to share playground", error);
setStatus("initial");
}
}}
>
<span
className="absolute inset-0 flex items-center justify-center"
aria-hidden="false"
>
Share
</span>
<span className="invisible" aria-hidden="true">
Copied!
</span>
</AstralButton>
const copied = status === "copied" && !isPending;

return (
<MenuTrigger>
<Pressable>
<AstralButton
type="button"
className={classNames(
"relative flex-none leading-6 py-1.5 px-3",
copied
? "cursor-auto dark:shadow-copied"
: "shadow-xs disabled:opacity-50",
)}
disabled={isPending}
>
<span
className={classNames(
"absolute inset-0 flex items-center justify-center",
copied && "invisible",
)}
aria-hidden={copied}
>
Share
</span>
<span
className={classNames(!copied && "invisible")}
aria-hidden={!copied}
>
Copied!
</span>
</AstralButton>
</Pressable>
<Popover className="min-w-[150px] bg-white dark:bg-galaxy border border-gray-200 dark:border-comet rounded-md shadow-lg mt-1 z-10">
<Menu className="font-sans p-1 outline-0 max-h-[inherit] overflow-auto">
<ShareMenuItem
onAction={() => startTransition(() => dispatch("share"))}
>
Link
</ShareMenuItem>
<ShareMenuItem
onAction={() => startTransition(() => dispatch("copyMarkdownLink"))}
>
Markdown Link
</ShareMenuItem>
<ShareMenuItem
onAction={() => startTransition(() => dispatch("copyMarkdown"))}
>
Markdown
</ShareMenuItem>
</Menu>
</Popover>
</MenuTrigger>
);
}

function ShareMenuItem({ className, ...props }: MenuItemProps) {
return (
<AriaMenuItem
className={classNames(
"px-3 py-1.5 text-sm cursor-pointer outline-0 rounded",
"text-galaxy dark:text-white",
"hover:bg-gray-100 dark:hover:bg-space",
className,
)}
{...props}
/>
);
}
1 change: 0 additions & 1 deletion playground/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export { ErrorMessage } from "./ErrorMessage";
export { default as Header } from "./Header";
export { default as RepoButton } from "./RepoButton";
export * as Icons from "./Icons";
export { default as ShareButton } from "./ShareButton";
export { type Theme, useTheme } from "./theme";
export { HorizontalResizeHandle, VerticalResizeHandle } from "./ResizeHandle";
export { setupMonaco } from "./setupMonaco";
Expand Down
2 changes: 1 addition & 1 deletion playground/ty/src/Editor/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const API_URL = import.meta.env.PROD
? "https://api.astral-1ad.workers.dev"
: "http://0.0.0.0:8787";
: "http://localhost:8787";

export type Playground = {
files: { [name: string]: string };
Expand Down
Loading
Loading