More playground polish
MODIFY
website/src/playground/CodeMirror.tsx
website/src/playground/CodeMirror.tsx
@@ -85,7 +85,7 @@ export default forwardRef<ReactCodeMirrorRef, Props>(function CodeMirror(
}
const extensions = useMemo(() => {
- if (diagnostics === undefined) {
+ if (diagnostics === undefined || diagnostics.length === 0) {
return getDefaultExtensions(props.extensions);
}

MODIFY
website/src/playground/Playground.tsx
website/src/playground/Playground.tsx
@@ -45,8 +45,6 @@ export default function PlaygroundLoader({
const romeOutput = file.rome;
const prettierOutput = file.prettier;
- console.log(file);
-
const codeMirrorExtensions = useMemo(
() => [
javascript({

MODIFY
website/src/playground/PlaygroundLoader.tsx
website/src/playground/PlaygroundLoader.tsx
-import { useEffect, useState, useRef } from "react";
-import { LoadingState } from "./types";
-import { getCurrentCode, getFileState, usePlaygroundState } from "./utils";
+import { useEffect, useState, useRef, SetStateAction, Dispatch } from "react";
+import {
+ defaultPlaygroundState,
+ emptyPrettierOutput,
+ emptyRomeOutput,
+ IndentStyle,
+ LoadingState,
+ PlaygroundSettings,
+ PlaygroundState,
+ QuoteProperties,
+ QuoteStyle,
+ TrailingComma,
+} from "./types";
+import {
+ createLocalStorage,
+ decodeCode,
+ encodeCode,
+ getCurrentCode,
+ getExtension,
+ getFileState,
+ isJSXFilename,
+ isScriptFilename,
+ isTypeScriptFilename,
+ normalizeFilename,
+} from "./utils";
import Playground from "./Playground";
import LoadingScreen from "./components/LoadingScreen";
@@ -153,4 +175,169 @@ function App() {
}
}
+const lastSearchStore = createLocalStorage("last-search");
+
+const FILE_QUERY_KEY_REGEX = /^files\.(.*?)$/;
+
+export function usePlaygroundState(): [
+ PlaygroundState,
+ Dispatch<SetStateAction<PlaygroundState>>,
+ () => void,
+] {
+ const searchQuery =
+ window.location.search === ""
+ ? lastSearchStore.get() ?? ""
+ : window.location.search;
+
+ function initState(
+ searchParams: URLSearchParams,
+ ignoreFiles: boolean,
+ ): PlaygroundState {
+ let singleFileMode = true;
+ let hasFiles = false;
+ let files: PlaygroundState["files"] = {};
+
+ if (!ignoreFiles) {
+ // Populate files
+ for (const [key, value] of searchParams) {
+ const match = key.match(FILE_QUERY_KEY_REGEX);
+ if (match != null) {
+ const filename = normalizeFilename(match[1]!);
+ files[filename] = {
+ content: decodeCode(value),
+ rome: emptyRomeOutput,
+ prettier: emptyPrettierOutput,
+ };
+ singleFileMode = false;
+ hasFiles = true;
+ }
+ }
+
+ // Single file mode
+ if (searchParams.get("code")) {
+ const ext = getExtension({
+ typescript: searchParams.get("typescript") === "true",
+ jsx: searchParams.get("jsx") === "true",
+ script: searchParams.get("script") === "true",
+ });
+ files[`main.${ext}`] = {
+ content: decodeCode(searchParams.get("code") ?? ""),
+ rome: emptyRomeOutput,
+ prettier: emptyPrettierOutput,
+ };
+ hasFiles = true;
+ }
+ }
+
+ if (!hasFiles) {
+ files = defaultPlaygroundState.files;
+ }
+
+ return {
+ cursorPosition: 0,
+ tab: searchParams.get("tab") ?? "formatter",
+ singleFileMode,
+ currentFile: Object.keys(files)[0] ?? "main.js",
+ files,
+ settings: {
+ lineWidth: parseInt(
+ searchParams.get("lineWidth") ??
+ String(defaultPlaygroundState.settings.lineWidth),
+ ),
+ indentStyle:
+ (searchParams.get("indentStyle") as IndentStyle) ??
+ defaultPlaygroundState.settings.indentStyle,
+ quoteStyle:
+ (searchParams.get("quoteStyle") as QuoteStyle) ??
+ defaultPlaygroundState.settings.quoteStyle,
+ quoteProperties:
+ (searchParams.get("quoteProperties") as QuoteProperties) ??
+ defaultPlaygroundState.settings.quoteProperties,
+ trailingComma:
+ (searchParams.get("trailingComma") as TrailingComma) ??
+ defaultPlaygroundState.settings.trailingComma,
+ indentWidth: parseInt(
+ searchParams.get("indentWidth") ??
+ String(defaultPlaygroundState.settings.indentWidth),
+ ),
+ enabledNurseryRules:
+ searchParams.get("enabledNurseryRules") === "true" ||
+ defaultPlaygroundState.settings.enabledNurseryRules,
+ enabledLinting:
+ searchParams.get("enabledLinting") === "true" ||
+ defaultPlaygroundState.settings.enabledLinting,
+ },
+ };
+ }
+
+ const [playgroundState, setPlaygroundState] = useState(() =>
+ initState(new URLSearchParams(searchQuery), window.location.search === ""),
+ );
+
+ function resetPlaygroundState() {
+ setPlaygroundState(initState(new URLSearchParams(""), false));
+ }
+
+ useEffect(() => {
+ const rawQueryParams: Record<string, unknown> = {
+ ...playgroundState.settings,
+ };
+
+ // Eliminate default values
+ const queryStringObj: Record<string, string> = {};
+ for (const key in rawQueryParams) {
+ const defaultValue = String(
+ defaultPlaygroundState.settings[key as keyof PlaygroundSettings],
+ );
+ const rawValue = rawQueryParams[key];
+ const value = String(rawValue);
+
+ if (rawValue !== undefined && value !== defaultValue) {
+ queryStringObj[key] = value;
+ }
+ }
+
+ if (
+ playgroundState.singleFileMode &&
+ Object.keys(playgroundState.files).length === 1
+ ) {
+ // Single file mode
+ const code = getCurrentCode(playgroundState);
+ if (code) {
+ queryStringObj.code = encodeCode(code);
+ }
+
+ if (!isTypeScriptFilename(playgroundState.currentFile)) {
+ queryStringObj.typescript = "false";
+ }
+
+ if (!isJSXFilename(playgroundState.currentFile)) {
+ queryStringObj.jsx = "false";
+ }
+
+ if (isScriptFilename(playgroundState.currentFile)) {
+ queryStringObj.script = "true";
+ }
+ } else {
+ // Populate files
+ for (const filename in playgroundState.files) {
+ const content = playgroundState.files[filename]?.content ?? "";
+ queryStringObj[`files.${filename}`] = encodeCode(content);
+ }
+ }
+
+ const queryString = new URLSearchParams(queryStringObj).toString();
+ lastSearchStore.set(queryString);
+
+ let url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
+ if (queryString !== "") {
+ url += `?${queryString}`;
+ }
+
+ window.history.replaceState({ path: url }, "", url);
+ }, [playgroundState]);
+
+ return [playgroundState, setPlaygroundState, resetPlaygroundState];
+}
+
export default App;

MODIFY
website/src/playground/styles/_results.scss
website/src/playground/styles/_results.scss
}
.mermaid {
- border-top: 1px solid var(--playground-border);
display: flex;
width: 100%;
height: 100%;

MODIFY
website/src/playground/styles/_settings.scss
website/src/playground/styles/_settings.scss
padding: 2px 6px;
color: inherit;
border-radius: 6px;
+ cursor: pointer;
@include dark-mode {
border-color: rgba(255, 255, 255, 0.1);
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.1) !important;
- border-top: 1px solid var(--playground-border);
}
.file-view {
}
> input[type=text] {
- width: 100%;
+ display: block;
padding: 5px;
margin: 5px;
+ margin-bottom: 0;
}
.files-list {
padding-top: 10px;
li {
- padding: 4px 10px;
+ display: flex;
+ align-items: center;
+ padding: 0 10px;
+ cursor: pointer;
+ line-height: 30px;
+ height: 30px;
+
+ &:hover {
+ background: rgba(#000, 5%);
+
+ button {
+ display: block;
+ opacity: 0.8;
+ }
+ }
&.active {
font-weight: bold;
background: rgba(#000, 10%);
}
+
+ button {
+ display: none;
+ margin-left: auto;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
}
}
}

MODIFY
website/src/playground/styles/_tabs.scss
website/src/playground/styles/_tabs.scss
overflow-y: hidden;
background-color: var(--container-color);
font-size: 14px;
+ border-bottom: 1px solid var(--playground-border);
@include dark-mode {
background-color: rgba(255, 255, 255, 0.2);

MODIFY
website/src/playground/styles/index.scss
website/src/playground/styles/index.scss
@@ -53,7 +53,11 @@ h2 {
font-size: 14px;
font-weight: normal;
box-shadow: inset 0 1px 0 var(--hard-border-color), inset 0 -1px 0 var(--hard-border-color);
- background-color: rgba(255, 255, 255, 0.15);
+ background-color: rgba(#000, 0.05);
+
+ @include dark-mode {
+ background-color: rgba(#fff, 0.15);
+ }
&.rome {
background-color: rgba($color-4, 30%);

MODIFY
website/src/playground/tabs/SettingsTab.tsx
website/src/playground/tabs/SettingsTab.tsx
@@ -16,6 +16,7 @@ import {
isTypeScriptFilename,
classnames,
getFileState,
+ normalizeFilename,
} from "../utils";
export interface SettingsTabProps {
@@ -97,16 +98,32 @@ export default function SettingsTab({
});
}
- function createFile(filename: string) {
- if (
- // rome-ignore lint(complexity/useSimplifiedLogicExpression): Not sure how else to do this
- !isScriptFilename(filename) &&
- !isJSXFilename(filename) &&
- !isTypeScriptFilename(filename)
- ) {
- filename = `${filename}.js`;
+ function deleteFile(filename: string) {
+ setPlaygroundState((state) => {
+ const { [filename]: _, ...files } = state.files;
+ let currentFile = state.currentFile;
+
+ if (currentFile === filename) {
+ const files = Object.keys(state.files);
+ const index = files.indexOf(filename);
+ currentFile = files[index + 1] ?? files[index - 1] ?? currentFile;
}
+ return {
+ ...state,
+ currentFile,
+ files: {
+ ...files,
+ // Make sure currentFile is still accessible
+ [currentFile]: getFileState(state, currentFile),
+ },
+ };
+ });
+ }
+
+ function createFile(filename: string) {
+ filename = normalizeFilename(filename);
+
setPlaygroundState((state) => ({
...state,
currentFile: filename,
@@ -117,6 +134,24 @@ export default function SettingsTab({
}));
}
+ function renameFile(oldFilename: string, newFilename: string) {
+ newFilename = normalizeFilename(newFilename);
+
+ setPlaygroundState((state) => {
+ const { [oldFilename]: oldFile, ...files } = state.files;
+
+ return {
+ ...state,
+ currentFile:
+ state.currentFile === oldFilename ? newFilename : state.currentFile,
+ files: {
+ ...files,
+ [newFilename]: oldFile,
+ },
+ };
+ });
+ }
+
function setCurrentFile(currentFile: string) {
setPlaygroundState((state) => ({
...state,
@@ -147,7 +182,9 @@ export default function SettingsTab({
currentFile={currentFile}
files={Object.keys(files)}
createFile={createFile}
+ deleteFile={deleteFile}
setCurrentFile={setCurrentFile}
+ renameFile={renameFile}
/>
)}
<FormatterSettings
@@ -178,11 +215,15 @@ export default function SettingsTab({
function FileView({
currentFile,
createFile,
+ deleteFile,
+ renameFile,
setCurrentFile,
files,
}: {
createFile: (filename: string) => void;
+ deleteFile: (filename: string) => void;
setCurrentFile: (filename: string) => void;
+ renameFile: (oldFilename: string, newFilename: string) => void;
currentFile: string;
files: string[];
}) {
@@ -205,17 +246,24 @@ function FileView({
key={i}
isActive={filename === currentFile}
filename={filename}
+ canDelete={files.length > 1}
onClick={() => {
setCurrentFile(filename);
}}
+ renameFile={(newFilename) => {
+ renameFile(filename, newFilename);
+ }}
+ deleteFile={() => {
+ deleteFile(filename);
+ }}
/>
);
})}
</ul>
{isCreatingFile && (
- <NewFileInput
- createFile={(filename) => {
+ <FilenameInput
+ onSubmit={(filename) => {
createFile(filename);
setCreatingFile(false);
}}
@@ -230,30 +278,78 @@ function FileViewItem({
filename,
isActive,
onClick,
+ deleteFile,
+ renameFile,
+ canDelete,
}: {
filename: string;
+ canDelete: boolean;
isActive: boolean;
+ renameFile: (newFilename: string) => void;
onClick: () => void;
+ deleteFile: () => void;
}) {
+ const [isRenaming, setIsRenaming] = useState(false);
+
+ const className = classnames(isActive && "active");
+
+ if (isRenaming) {
return (
- <li
- className={classnames(isActive && "active")}
- onClick={onClick}
- onKeyDown={onClick}
- >
+ <li>
+ <FilenameInput
+ onSubmit={(newFilename) => {
+ renameFile(newFilename);
+ setIsRenaming(false);
+ }}
+ onCancel={() => {
+ setIsRenaming(false);
+ }}
+ initialValue={filename}
+ />
+ </li>
+ );
+ }
+
+ function onDeleteClick(e: React.MouseEvent | React.KeyboardEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ deleteFile();
+ }
+
+ function onRenameClick(e: React.MouseEvent | React.KeyboardEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsRenaming(true);
+ }
+
+ return (
+ <li className={className} onClick={onClick} onKeyDown={onClick}>
{filename}
+
+ <button onClick={onRenameClick} onKeyDown={onRenameClick}>
+ Rename
+ </button>
+
+ {canDelete && (
+ <button onClick={onDeleteClick} onKeyDown={onDeleteClick}>
+ <span className="sr-only">Delete</span>
+ <span aria-hidden={true}>X</span>
+ </button>
+ )}
</li>
);
}
-function NewFileInput({
- createFile,
+function FilenameInput({
+ onSubmit,
onCancel,
+ initialValue,
}: {
- createFile: (filename: string) => void;
+ onSubmit: (filename: string) => void;
onCancel: () => void;
+ initialValue?: string;
}) {
- const [filename, setFilename] = useState("");
+ const [filename, setFilename] = useState(initialValue ?? "");
function onKeyDown(e: React.KeyboardEvent) {
if (e.key === "Escape") {
@@ -261,7 +357,7 @@ function NewFileInput({
}
if (e.key === "Enter") {
- createFile(filename);
+ onSubmit(filename);
}
}
@@ -269,7 +365,7 @@ function NewFileInput({
if (filename === "") {
onCancel();
} else {
- createFile(filename);
+ onSubmit(filename);
}
}

MODIFY
website/src/playground/types.ts
website/src/playground/types.ts
@@ -102,7 +102,7 @@ export interface PlaygroundState {
singleFileMode: boolean;
tab: string;
cursorPosition: number;
- files: Record<string, PlaygroundFileState>;
+ files: Record<string, undefined | PlaygroundFileState>;
settings: PlaygroundSettings;
}

MODIFY
website/src/playground/utils.ts
website/src/playground/utils.ts
@@ -9,7 +9,6 @@ import {
QuoteStyle,
QuoteProperties,
TrailingComma,
- defaultPlaygroundState,
PrettierOutput,
PlaygroundSettings,
emptyPrettierOutput,
@@ -105,171 +104,6 @@ export function createLocalStorage(name: string): {
};
}
-const lastSearchStore = createLocalStorage("last-search");
-
-const FILE_QUERY_KEY_REGEX = /^files\.(.*?)$/;
-
-export function usePlaygroundState(): [
- PlaygroundState,
- Dispatch<SetStateAction<PlaygroundState>>,
- () => void,
-] {
- const searchQuery =
- window.location.search === ""
- ? lastSearchStore.get() ?? ""
- : window.location.search;
-
- function initState(
- searchParams: URLSearchParams,
- ignoreFiles: boolean,
- ): PlaygroundState {
- let singleFileMode = true;
- let hasFiles = false;
- let files: PlaygroundState["files"] = {};
-
- if (!ignoreFiles) {
- // Populate files
- for (const [key, value] of searchParams) {
- const match = key.match(FILE_QUERY_KEY_REGEX);
- if (match != null) {
- const filename = match[1]!;
- files[filename] = {
- content: decodeCode(value),
- rome: emptyRomeOutput,
- prettier: emptyPrettierOutput,
- };
- singleFileMode = false;
- hasFiles = true;
- }
- }
-
- // Single file mode
- if (searchParams.get("code")) {
- const ext = getExtension({
- typescript: searchParams.get("typescript") === "true",
- jsx: searchParams.get("jsx") === "true",
- script: searchParams.get("script") === "true",
- });
- files[`main.${ext}`] = {
- content: decodeCode(searchParams.get("code") ?? ""),
- rome: emptyRomeOutput,
- prettier: emptyPrettierOutput,
- };
- hasFiles = true;
- }
- }
-
- if (!hasFiles) {
- files = defaultPlaygroundState.files;
- }
-
- return {
- cursorPosition: 0,
- tab: searchParams.get("tab") ?? "formatter",
- singleFileMode,
- currentFile: Object.keys(files)[0] ?? "main.js",
- files,
- settings: {
- lineWidth: parseInt(
- searchParams.get("lineWidth") ??
- String(defaultPlaygroundState.settings.lineWidth),
- ),
- indentStyle:
- (searchParams.get("indentStyle") as IndentStyle) ??
- defaultPlaygroundState.settings.indentStyle,
- quoteStyle:
- (searchParams.get("quoteStyle") as QuoteStyle) ??
- defaultPlaygroundState.settings.quoteStyle,
- quoteProperties:
- (searchParams.get("quoteProperties") as QuoteProperties) ??
- defaultPlaygroundState.settings.quoteProperties,
- trailingComma:
- (searchParams.get("trailingComma") as TrailingComma) ??
- defaultPlaygroundState.settings.trailingComma,
- indentWidth: parseInt(
- searchParams.get("indentWidth") ??
- String(defaultPlaygroundState.settings.indentWidth),
- ),
- enabledNurseryRules:
- searchParams.get("enabledNurseryRules") === "true" ||
- defaultPlaygroundState.settings.enabledNurseryRules,
- enabledLinting:
- searchParams.get("enabledLinting") === "true" ||
- defaultPlaygroundState.settings.enabledLinting,
- },
- };
- }
-
- const [playgroundState, setPlaygroundState] = useState(() =>
- initState(new URLSearchParams(searchQuery), window.location.search === ""),
- );
-
- function resetPlaygroundState() {
- setPlaygroundState(initState(new URLSearchParams(""), false));
- }
-
- useEffect(() => {
- const rawQueryParams: Record<string, unknown> = {
- ...playgroundState.settings,
- };
-
- // Eliminate default values
- const queryStringObj: Record<string, string> = {};
- for (const key in rawQueryParams) {
- const defaultValue = String(
- defaultPlaygroundState.settings[key as keyof PlaygroundSettings],
- );
- const rawValue = rawQueryParams[key];
- const value = String(rawValue);
-
- if (rawValue !== undefined && value !== defaultValue) {
- queryStringObj[key] = value;
- }
- }
-
- if (
- playgroundState.singleFileMode &&
- Object.keys(playgroundState.files).length === 1
- ) {
- // Single file mode
- const code = getCurrentCode(playgroundState);
- if (code) {
- queryStringObj.code = encodeCode(code);
- }
-
- if (!isTypeScriptFilename(playgroundState.currentFile)) {
- queryStringObj.typescript = "false";
- }
-
- if (!isJSXFilename(playgroundState.currentFile)) {
- queryStringObj.jsx = "false";
- }
-
- if (isScriptFilename(playgroundState.currentFile)) {
- queryStringObj.script = "true";
- }
- } else {
- // Populate files
- for (const filename in playgroundState.files) {
- const content = playgroundState.files[filename]?.content ?? "";
- queryStringObj[`files.${filename}`] = encodeCode(content);
- }
- }
-
- const queryString = new URLSearchParams(queryStringObj).toString();
- lastSearchStore.set(queryString);
-
- let url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
- if (queryString !== "") {
- url += `?${queryString}`;
- }
-
- window.history.replaceState({ path: url }, "", url);
- }, [playgroundState]);
-
- return [playgroundState, setPlaygroundState, resetPlaygroundState];
-}
-
export function getCurrentCode(state: PlaygroundState): string {
return state.files[state.currentFile]?.content ?? "";
}
@@ -481,3 +315,15 @@ export function getExtension(opts: ExtensionOptions): string {
return ext;
}
+
+export function isValidExtension(filename: string): boolean {
+ return (
+ isScriptFilename(filename) ||
+ isTypeScriptFilename(filename) ||
+ isJSXFilename(filename)
+ );
+}
+
+export function normalizeFilename(filename: string): string {
+ return isValidExtension(filename) ? filename : `${filename}.js`;
+}

More playground polish