From 7eedb07dab19ec41ed18ca74d0304dfcc0479ab0 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 11 Apr 2026 07:08:57 +0000 Subject: [PATCH 1/2] _ --- .oxlintrc.json | 22 ++- website/src/components/playground/App.tsx | 177 ++++++++++-------- website/src/components/playground/Editor.tsx | 6 +- .../components/playground/ErrorBoundary.tsx | 41 ++++ .../src/components/playground/compile-ts.ts | 47 ++--- .../src/components/playground/lua-worker.ts | 88 ++++++--- .../src/components/playground/playground.css | 37 ++++ website/tsconfig.json | 4 +- 8 files changed, 281 insertions(+), 141 deletions(-) create mode 100644 website/src/components/playground/ErrorBoundary.tsx diff --git a/.oxlintrc.json b/.oxlintrc.json index 6c23f92..b4bdfec 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,7 +8,7 @@ }, "rules": { "react/rules-of-hooks": "error", - "react/exhaustive-deps": "warn", + "react/exhaustive-deps": "error", "react/react-in-jsx-scope": "off", "react/no-array-index-key": "off", "import/no-unassigned-import": "off", @@ -16,7 +16,25 @@ "promise/always-return": "off", "promise/no-multiple-resolved": "off", "jsx-a11y/no-static-element-interactions": "off", - "unicorn/require-post-message-target-origin": "off" + "unicorn/require-post-message-target-origin": "off", + "typescript/ban-ts-comment": [ + "error", + { "ts-ignore": "allow-with-description", "ts-expect-error": "allow-with-description" } + ], + "typescript/no-misused-promises": "error", + "typescript/consistent-type-assertions": [ + "warn", + { "assertionStyle": "as", "objectLiteralTypeAssertions": "allow-as-parameter" } + ], + "typescript/no-unnecessary-condition": "warn" }, + "overrides": [ + { + "files": ["website/src/**/*.{ts,tsx}"], + "rules": { + "typescript/no-explicit-any": "error" + } + } + ], "ignorePatterns": ["node_modules", "extern", "tmp", "website/src/assets/types"] } diff --git a/website/src/components/playground/App.tsx b/website/src/components/playground/App.tsx index 618b98f..4412bef 100644 --- a/website/src/components/playground/App.tsx +++ b/website/src/components/playground/App.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { loader } from "@monaco-editor/react"; -import type { editor as monacoEditor } from "monaco-editor"; import { Editor } from "./Editor"; +import { ErrorBoundary } from "./ErrorBoundary"; import { OutputPanel } from "./OutputPanel"; import { loadWasm, transpile, type WasmDiagnostic } from "./wasm"; import { execJs, type ExecResult } from "./exec-js"; @@ -141,6 +141,14 @@ function ConfigToggle({ } export function App() { + return ( + + + + ); +} + +function PlaygroundApp() { const theme = useStarlightTheme(); const [pgState, setPgState, hashReady] = useHashState({ code: DEFAULT_CODE, @@ -166,13 +174,29 @@ export function App() { const [staleLuaEval, setStaleLuaEval] = useState(false); const [jsEvalMs, setJsEvalMs] = useState(null); const [luaEvalMs, setLuaEvalMs] = useState(null); - const tsEditorRef = useRef(null); const debounceRef = useRef>(null); const execDebounceRef = useRef>(null); + // Live ref to the current tsconfig so async callbacks (debounced + // handleTsChange) read the latest value instead of a captured snapshot. + const tsconfigRef = useRef(tsconfig); + useEffect(() => { + tsconfigRef.current = tsconfig; + }, [tsconfig]); // Monotonic epoch: bumped on every doTranspile entry. Async continuations // capture the epoch at the start of their path and check it before any // setState so stale results from slower prior runs can't overwrite newer ones. const epochRef = useRef(0); + // True while the component is mounted. Async callbacks check this before + // touching state so resolutions that arrive after unmount are no-ops. + const mountedRef = useRef(true); + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + if (debounceRef.current) clearTimeout(debounceRef.current); + if (execDebounceRef.current) clearTimeout(execDebounceRef.current); + }; + }, []); const [colPct, setColPct] = useState(50); const [rowPct, setRowPct] = useState(60); const gridRef = useRef(null); @@ -208,18 +232,21 @@ export function App() { useEffect(() => { loadWasm() - .then(() => setLoading(false)) - .catch((err) => { + .then(() => { + if (mountedRef.current) setLoading(false); + }) + .catch((err: Error) => { + if (!mountedRef.current) return; setErrors([`Failed to load WASM: ${err.message}`]); setLoading(false); }); }, []); const runExecution = useCallback( - async (epoch: number, tsSource: string, lua: string, tgt: string, tsTarget?: string) => { + async (epoch: number, tsSource: string, lua: string, tgt: string) => { const isCurrent = () => epoch === epochRef.current; try { - const js = await compileTs(tsSource, tsTarget); + const js = await compileTs(tsSource); if (!isCurrent()) return; const t0 = performance.now(); const result = await execJs(js); @@ -259,11 +286,21 @@ export function App() { const monacoRef = useRef> | null>(null); const extraLibsRef = useRef<{ dispose(): void }[]>([]); + // Cached key of the last-applied Monaco config. Skip re-apply when the + // relevant inputs (TS target, enabled types, lua target) haven't changed; + // otherwise Monaco flashes squigglies every keystroke during the async + // gap between disposing old libs and adding new ones. + const monacoKeyRef = useRef(""); const syncMonacoOptions = useCallback(async (cfg: PlaygroundTsconfig) => { const monaco = monacoRef.current; if (!monaco) return; const target = (cfg.compilerOptions?.target as string) || "ESNext"; + const types = (cfg.types ?? []).toSorted(); + const tgt = cfg.tstl?.luaTarget || "JIT"; + const key = JSON.stringify({ target, types, luaTarget: tgt }); + if (key === monacoKeyRef.current) return; + const targetMap: Record = { ES5: 1, ES2015: 2, @@ -279,44 +316,47 @@ export function App() { ES2025: 12, ESNext: 99, }; + + // Pre-load all lib content before touching Monaco state so the swap is + // atomic: Monaco's TS checker never sees a window with libs missing. + const libs: { content: string; name: string }[] = []; + if (types.includes("console")) libs.push({ content: CONSOLE_DTS, name: "console" }); + if (types.includes("language-extensions")) + libs.push({ content: langExtDts, name: "language-extensions" }); + if (types.includes("lua-types")) { + const dts = await getLuaTypesDts(tgt); + libs.push({ content: dts, name: "lua-types" }); + } + + // Bail if another sync superseded us during the await. + if (key === monacoKeyRef.current) return; + monacoKeyRef.current = key; + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(), target: targetMap[target] ?? 99, lib: [target.toLowerCase()], strict: true, }); - // Sync extra type libs for (const d of extraLibsRef.current) d.dispose(); - extraLibsRef.current = []; - const types = cfg.types ?? []; - const addLib = (content: string, name: string) => { - const d = monaco.languages.typescript.typescriptDefaults.addExtraLib( - content, - `file:///${name}.d.ts`, - ); - extraLibsRef.current.push(d); - }; - if (types.includes("console")) addLib(CONSOLE_DTS, "console"); - if (types.includes("language-extensions")) addLib(langExtDts, "language-extensions"); - if (types.includes("lua-types")) { - const tgt = cfg.tstl?.luaTarget || "JIT"; - const dts = await getLuaTypesDts(tgt); - addLib(dts, "lua-types"); - } + extraLibsRef.current = libs.map(({ content, name }) => + monaco.languages.typescript.typescriptDefaults.addExtraLib(content, `file:///${name}.d.ts`), + ); }, []); useEffect(() => { - loader.init().then((m) => { - monacoRef.current = m; - syncMonacoOptions(tsconfig); + // Only set monacoRef here. The initial doTranspile (fired from the + // [loading, hashReady] effect below) handles syncMonacoOptions with the + // restored-from-hash tsconfig, so we don't risk applying a stale snapshot. + void loader.init().then((m) => { + if (mountedRef.current) monacoRef.current = m; }); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + }, []); const setTsluaMarkers = useCallback((diagnostics: WasmDiagnostic[]) => { - const editor = tsEditorRef.current; const monaco = monacoRef.current; - if (!editor || !monaco) return; - const model = editor.getModel(); + if (!monaco) return; + const model = monaco.editor.getModel(monaco.Uri.parse("file:///main.ts")); if (!model) return; const markers = diagnostics.map((d) => ({ startLineNumber: d.startLine, @@ -351,7 +391,7 @@ export function App() { const result = transpile(code, { compilerOptions: { ...cfg.compilerOptions, lib }, extraFiles, - tstl: cfg.tstl, + ...(cfg.tstl ? { tstl: cfg.tstl } : {}), }); if (!isCurrent()) return; setTranspileMs(performance.now() - t0); @@ -362,10 +402,9 @@ export function App() { setStaleJs(true); setStaleLuaEval(true); if (execDebounceRef.current) clearTimeout(execDebounceRef.current); - const tsTarget = (cfg.compilerOptions?.target as string) || undefined; execDebounceRef.current = setTimeout(() => { if (!isCurrent()) return; - runExecution(epoch, code, result.lua, tgt, tsTarget); + runExecution(epoch, code, result.lua, tgt); }, 500); }, [loading, runExecution, setTsluaMarkers, syncMonacoOptions], @@ -382,60 +421,53 @@ export function App() { setStaleJs(true); setStaleLuaEval(true); if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => doTranspile(value, tsconfig), 300); + debounceRef.current = setTimeout(() => doTranspile(value, tsconfigRef.current), 300); }, - [tsconfig, doTranspile, setPgState], + [doTranspile, setPgState], ); const updateTstl = useCallback( (key: string, value: string | boolean) => { - setPgState((prev) => { - const tstl = { ...prev.tsconfig.tstl, [key]: value }; - // Clean up default/falsy values - if (value === "" || value === false) delete (tstl as Record)[key]; - const next: PlaygroundState = { - ...prev, - tsconfig: { ...prev.tsconfig, tstl }, - }; - doTranspile(prev.code, next.tsconfig); - return next; - }); + const tstl = { ...pgState.tsconfig.tstl, [key]: value }; + if (value === "" || value === false) delete (tstl as Record)[key]; + const next: PlaygroundState = { + ...pgState, + tsconfig: { ...pgState.tsconfig, tstl }, + }; + setPgState(next); + doTranspile(next.code, next.tsconfig); }, - [doTranspile, setPgState], + [pgState, setPgState, doTranspile], ); const updateCompilerOption = useCallback( (key: string, value: string) => { - setPgState((prev) => { - const co = { ...prev.tsconfig.compilerOptions, [key]: value }; - if (value === "") delete (co as Record)[key]; - const next: PlaygroundState = { - ...prev, - tsconfig: { ...prev.tsconfig, compilerOptions: co }, - }; - doTranspile(prev.code, next.tsconfig); - return next; - }); + const co = { ...pgState.tsconfig.compilerOptions, [key]: value }; + if (value === "") delete (co as Record)[key]; + const next: PlaygroundState = { + ...pgState, + tsconfig: { ...pgState.tsconfig, compilerOptions: co }, + }; + setPgState(next); + doTranspile(next.code, next.tsconfig); }, - [doTranspile, setPgState], + [pgState, setPgState, doTranspile], ); const toggleType = useCallback( (name: string) => { - setPgState((prev) => { - const types = prev.tsconfig.types ?? []; - const next: PlaygroundState = { - ...prev, - tsconfig: { - ...prev.tsconfig, - types: types.includes(name) ? types.filter((t) => t !== name) : [...types, name], - }, - }; - doTranspile(prev.code, next.tsconfig); - return next; - }); + const types = pgState.tsconfig.types ?? []; + const next: PlaygroundState = { + ...pgState, + tsconfig: { + ...pgState.tsconfig, + types: types.includes(name) ? types.filter((t) => t !== name) : [...types, name], + }, + }; + setPgState(next); + doTranspile(next.code, next.tsconfig); }, - [doTranspile, setPgState], + [pgState, setPgState, doTranspile], ); const sidebar = ( @@ -566,9 +598,6 @@ export function App() { language="typescript" path="file:///main.ts" onChange={handleTsChange} - onEditorMount={(e) => { - tsEditorRef.current = e; - }} theme={theme} /> diff --git a/website/src/components/playground/Editor.tsx b/website/src/components/playground/Editor.tsx index 20e129c..58b0db6 100644 --- a/website/src/components/playground/Editor.tsx +++ b/website/src/components/playground/Editor.tsx @@ -43,13 +43,13 @@ export function Editor({ { + override state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + override componentDidCatch(error: Error, info: ErrorInfo): void { + console.error("Playground crashed:", error, info.componentStack); + } + + override render(): ReactNode { + const { error } = this.state; + if (!error) return this.props.children; + return ( +
+

Playground crashed

+

Something went wrong while rendering the playground.

+
{error.message}
+ {error.stack &&
{error.stack}
} + +
+ ); + } +} diff --git a/website/src/components/playground/compile-ts.ts b/website/src/components/playground/compile-ts.ts index 274d425..85fd0ae 100644 --- a/website/src/components/playground/compile-ts.ts +++ b/website/src/components/playground/compile-ts.ts @@ -1,43 +1,26 @@ // Uses Monaco's built-in TypeScript compiler to emit JS from TS source. -// Owns its own model so compilation works even when no editor is mounted -// (e.g. on mobile when the user has switched to a non-TS tab). +// Shares the editor's model (file:///main.ts) when mounted, and creates a +// stand-in at the same URI when not (e.g. on mobile with a non-TS tab active). +// Using the same URI as the editor avoids creating a second TS model, which +// would otherwise collide with it in TypeScript's global scope and produce +// spurious "Cannot redeclare block-scoped variable" diagnostics. import { loader } from "@monaco-editor/react"; -const MODEL_URI = "file:///compile-ts-source.ts"; - -// Map our target strings to Monaco's ScriptTarget enum values. -const TARGET_MAP: Record = { - ES5: 1, - ES2015: 2, - ES2016: 3, - ES2017: 4, - ES2018: 5, - ES2019: 6, - ES2020: 7, - ES2021: 8, - ES2022: 9, - ES2023: 10, - ES2024: 11, - ESNext: 99, -}; - -export async function compileTs(source: string, target?: string): Promise { +const MODEL_URI = "file:///main.ts"; + +export async function compileTs(source: string): Promise { const monaco = await loader.init(); const uri = monaco.Uri.parse(MODEL_URI); - const scriptTarget = (target && TARGET_MAP[target]) ?? TARGET_MAP.ESNext; - monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ - ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(), - target: scriptTarget, - lib: [(target || "ESNext").toLowerCase()], - }); - - // Get or create our own model so compilation is independent of any editor. let model = monaco.editor.getModel(uri); if (!model) { model = monaco.editor.createModel(source, "typescript", uri); - } else if (model.getValue() !== source) { + } else if (!model.isAttachedToEditor() && model.getValue() !== source) { + // Editor unmounted (e.g. mobile non-TS tab): compileTs owns the model and + // must push updates into it. When an editor is attached, the editor owns + // the model and we leave it alone — calling setValue would reset the + // user's cursor and selection. model.setValue(source); } @@ -45,7 +28,9 @@ export async function compileTs(source: string, target?: string): Promise f.name.endsWith(".js")); + const jsFile = (output.outputFiles as { name: string; text: string }[]).find((f) => + f.name.endsWith(".js"), + ); if (!jsFile) { throw new Error("TypeScript compilation produced no output"); diff --git a/website/src/components/playground/lua-worker.ts b/website/src/components/playground/lua-worker.ts index 42d43ee..5193e8d 100644 --- a/website/src/components/playground/lua-worker.ts +++ b/website/src/components/playground/lua-worker.ts @@ -2,35 +2,59 @@ // Receives { code, target } messages, returns { raw, pretty } results. // All WASM loading and execution happens here. -// @ts-ignore — CJS module +// lua-wasm-bindings has no declarations for the binding-factory entry but +// does export Emscripten module types we can use for the glue factories. + +// @ts-expect-error — binding-factory has no declarations upstream import { createLua, createLauxLib, createLuaLib } from "lua-wasm-bindings/dist/binding-factory"; +import type { LuaEmscriptenModule } from "lua-wasm-bindings/dist/glue/glue"; -// @ts-ignore import glueFactory50 from "lua-wasm-bindings/dist/glue/glue-lua-5.0.3.js"; -// @ts-ignore import glueFactory51 from "lua-wasm-bindings/dist/glue/glue-lua-5.1.5.js"; -// @ts-ignore import glueFactory52 from "lua-wasm-bindings/dist/glue/glue-lua-5.2.4.js"; -// @ts-ignore import glueFactory53 from "lua-wasm-bindings/dist/glue/glue-lua-5.3.6.js"; -// @ts-ignore import glueFactory54 from "lua-wasm-bindings/dist/glue/glue-lua-5.4.7.js"; -// @ts-ignore import glueFactory55 from "lua-wasm-bindings/dist/glue/glue-lua-5.5.0.js"; -// @ts-ignore import wasm50 from "lua-wasm-bindings/dist/glue/glue-lua-5.0.3.wasm?url"; -// @ts-ignore import wasm51 from "lua-wasm-bindings/dist/glue/glue-lua-5.1.5.wasm?url"; -// @ts-ignore import wasm52 from "lua-wasm-bindings/dist/glue/glue-lua-5.2.4.wasm?url"; -// @ts-ignore import wasm53 from "lua-wasm-bindings/dist/glue/glue-lua-5.3.6.wasm?url"; -// @ts-ignore import wasm54 from "lua-wasm-bindings/dist/glue/glue-lua-5.4.7.wasm?url"; -// @ts-ignore import wasm55 from "lua-wasm-bindings/dist/glue/glue-lua-5.5.0.wasm?url"; +// --- Narrow types for the bindings surface we use --- + +type LuaState = number; + +interface LuaLib { + lua_tostring(L: LuaState, idx: number): string; + lua_close(L: LuaState): void; +} + +interface LauxLib { + luaL_newstate(): LuaState; + luaL_dostring(L: LuaState, code: string): number; +} + +interface LuaStdLib { + luaL_openlibs(L: LuaState): void; +} + +type GlueFactory = (opts: { + wasmBinary: Uint8Array; + print(text: string): void; + printErr(text: string): void; +}) => LuaEmscriptenModule; + +const createLuaTyped = createLua as (glue: LuaEmscriptenModule, semver: string) => LuaLib; +const createLauxLibTyped = createLauxLib as ( + glue: LuaEmscriptenModule, + lua: LuaLib, + semver: string, +) => LauxLib; +const createLuaLibTyped = createLuaLib as (glue: LuaEmscriptenModule, semver: string) => LuaStdLib; + interface ExecResult { output: string[]; error: string | null; @@ -149,32 +173,33 @@ const TARGET_TO_VERSION: Record = { }; interface VersionInfo { - factory: any; + factory: GlueFactory | { default: GlueFactory }; wasmUrl: string; semver: string; } const VERSIONS: Record = { - "5.0": { factory: glueFactory50, wasmUrl: wasm50, semver: "5.0.3" }, - "5.1": { factory: glueFactory51, wasmUrl: wasm51, semver: "5.1.5" }, - "5.2": { factory: glueFactory52, wasmUrl: wasm52, semver: "5.2.4" }, - "5.3": { factory: glueFactory53, wasmUrl: wasm53, semver: "5.3.6" }, - "5.4": { factory: glueFactory54, wasmUrl: wasm54, semver: "5.4.7" }, - "5.5": { factory: glueFactory55, wasmUrl: wasm55, semver: "5.5.0" }, + "5.0": { factory: glueFactory50 as GlueFactory, wasmUrl: wasm50 as string, semver: "5.0.3" }, + "5.1": { factory: glueFactory51 as GlueFactory, wasmUrl: wasm51 as string, semver: "5.1.5" }, + "5.2": { factory: glueFactory52 as GlueFactory, wasmUrl: wasm52 as string, semver: "5.2.4" }, + "5.3": { factory: glueFactory53 as GlueFactory, wasmUrl: wasm53 as string, semver: "5.3.6" }, + "5.4": { factory: glueFactory54 as GlueFactory, wasmUrl: wasm54 as string, semver: "5.4.7" }, + "5.5": { factory: glueFactory55 as GlueFactory, wasmUrl: wasm55 as string, semver: "5.5.0" }, }; let currentOutput: string[] = []; interface LuaModule { - lua: any; - lauxlib: any; - lualib: any; + lua: LuaLib; + lauxlib: LauxLib; + lualib: LuaStdLib; } const moduleCache = new Map(); async function getLuaModule(version: string): Promise { - if (moduleCache.has(version)) return moduleCache.get(version)!; + const cached = moduleCache.get(version); + if (cached) return cached; const ver = VERSIONS[version]; if (!ver) throw new Error(`No Lua ${version} WASM available`); @@ -182,7 +207,10 @@ async function getLuaModule(version: string): Promise { const wasmResponse = await fetch(ver.wasmUrl); const wasmBinary = new Uint8Array(await wasmResponse.arrayBuffer()); - const factory = ver.factory.default ?? ver.factory; + const factory: GlueFactory = + "default" in ver.factory && typeof ver.factory.default === "function" + ? ver.factory.default + : (ver.factory as GlueFactory); const luaGlue = factory({ wasmBinary, print: (text: string) => { @@ -193,16 +221,16 @@ async function getLuaModule(version: string): Promise { }, }); - const lua = createLua(luaGlue, ver.semver); - const lauxlib = createLauxLib(luaGlue, lua, ver.semver); - const lualib = createLuaLib(luaGlue, ver.semver); + const lua = createLuaTyped(luaGlue, ver.semver); + const lauxlib = createLauxLibTyped(luaGlue, lua, ver.semver); + const lualib = createLuaLibTyped(luaGlue, ver.semver); - const mod = { lua, lauxlib, lualib }; + const mod: LuaModule = { lua, lauxlib, lualib }; moduleCache.set(version, mod); return mod; } -function runOnce(lua: any, lauxlib: any, lualib: any, code: string): ExecResult { +function runOnce(lua: LuaLib, lauxlib: LauxLib, lualib: LuaStdLib, code: string): ExecResult { currentOutput = []; try { const L = lauxlib.luaL_newstate(); diff --git a/website/src/components/playground/playground.css b/website/src/components/playground/playground.css index a50159b..e6585a3 100644 --- a/website/src/components/playground/playground.css +++ b/website/src/components/playground/playground.css @@ -98,6 +98,43 @@ color: var(--pg-text-muted); } +.pg-crash { + padding: 1.5rem; + max-width: 48rem; + margin: 2rem auto; + font-family: system-ui, sans-serif; + color: var(--pg-text); +} +.pg-crash h2 { + margin: 0 0 0.5rem; + font-size: 1.25rem; +} +.pg-crash-msg, +.pg-crash-stack { + background: var(--pg-bg-cell, #1e1e1e); + border: 1px solid var(--pg-border, #444); + border-radius: 4px; + padding: 0.75rem; + overflow: auto; + white-space: pre-wrap; + font-family: ui-monospace, monospace; + font-size: 0.85rem; + margin: 0.5rem 0; +} +.pg-crash-stack { + max-height: 24rem; +} +.pg-crash-reset { + margin-top: 0.5rem; + padding: 0.4rem 0.9rem; + background: var(--pg-accent, #3b82f6); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} + /* ---- Mobile tabs ---- */ .pg-mobile-tabs { display: flex; diff --git a/website/tsconfig.json b/website/tsconfig.json index 56c763a..6749eca 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -5,7 +5,9 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitOverride": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "exactOptionalPropertyTypes": true }, "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist", "astro.config.mjs"] From 8927565dc2cc1f8cf835ef5c4dc6d1014878f4c6 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 11 Apr 2026 07:15:02 +0000 Subject: [PATCH 2/2] _ --- website/src/components/playground/playground.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/website/src/components/playground/playground.css b/website/src/components/playground/playground.css index e6585a3..cd0157c 100644 --- a/website/src/components/playground/playground.css +++ b/website/src/components/playground/playground.css @@ -217,13 +217,15 @@ display: flex; align-items: center; justify-content: space-between; - padding: 0.75rem; + height: 2.625rem; + padding-inline: 0.75rem; font-size: 0.75rem; font-weight: 600; color: var(--pg-text-muted); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--pg-border); + flex-shrink: 0; } .pg-sidebar-collapse { background: none; @@ -354,7 +356,8 @@ display: none; align-items: center; gap: 0.5rem; - padding: 0.75rem; + height: 2.625rem; + padding-inline: 0.75rem; font-size: 0.75rem; font-weight: 600; color: var(--pg-text-muted);