add localisation and theming for playground

This commit is contained in:
Moritz Hölting 2024-06-22 14:36:30 +02:00
parent 1a5dcd24bc
commit a02af4f90c
15 changed files with 403 additions and 136 deletions

View File

@ -1,78 +1,114 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import starlight from '@astrojs/starlight'; import starlight from "@astrojs/starlight";
import react from "@astrojs/react"; import react from "@astrojs/react";
import starlightLinksValidator from "starlight-links-validator"; import starlightLinksValidator from "starlight-links-validator";
import shikiConfig from './src/utils/shiki'; import starlightUtils from "@lorenzo_lewis/starlight-utils";
import shikiConfig from "./src/utils/shiki";
const playgroundSidebarEntry = {
label: "Playground",
link: "/playground",
translations: { de: "Spielplatz" },
};
const navLinks = [playgroundSidebarEntry];
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [react(), starlight({ integrations: [
title: 'ShulkerScript', react(),
logo: { starlight({
src: './src/assets/logo.webp', title: "ShulkerScript",
alt: 'ShulkerScript Logo' logo: {
}, src: "./src/assets/logo.webp",
favicon: '/favicon.ico', alt: "ShulkerScript Logo",
description: 'A simple and powerful scripting language for Minecraft datapacks.', },
social: { favicon: "/favicon.ico",
github: 'https://github.com/moritz-hoelting/shulkerscript-cli' description:
}, "A simple and powerful scripting language for Minecraft datapacks.",
tableOfContents: { social: {
minHeadingLevel: 1, github: "https://github.com/moritz-hoelting/shulkerscript-cli",
maxHeadingLevel: 3 },
}, tableOfContents: {
defaultLocale: 'root', minHeadingLevel: 1,
locales: { maxHeadingLevel: 3,
root: { },
label: 'English', defaultLocale: "root",
lang: 'en' locales: {
}, root: {
de: { label: "English",
label: 'Deutsch', lang: "en",
lang: 'de' },
} de: {
}, label: "Deutsch",
editLink: { lang: "de",
baseUrl: 'https://github.com/moritz-hoelting/shulkerscript-webpage/edit/main' },
}, },
customCss: ['./src/styles/style.css'], editLink: {
plugins: [starlightLinksValidator({ baseUrl:
errorOnFallbackPages: false "https://github.com/moritz-hoelting/shulkerscript-webpage/edit/main",
})], },
expressiveCode: { customCss: ["./src/styles/style.css"],
shiki: shikiConfig plugins: [
}, starlightUtils({
components: { navLinks: {
PageTitle: './src/components/override/PageTitle.astro', leading: {
ContentPanel: './src/components/override/ContentPanel.astro', useSidebarLabelled: "leadingNavLinks",
}, },
sidebar: [{ },
label: 'Guides', }),
autogenerate: { starlightLinksValidator({
directory: 'guides' errorOnFallbackPages: false,
}, }),
translations: { ],
de: 'Anleitungen' expressiveCode: {
} shiki: shikiConfig,
}, { },
label: 'Roadmap', components: {
link: '/roadmap', PageTitle: "./src/components/override/PageTitle.astro",
translations: { ContentPanel: "./src/components/override/ContentPanel.astro",
de: 'Zukunftspläne' },
} sidebar: [
}, { {
label: 'Reference', label: "leadingNavLinks",
autogenerate: { items: navLinks,
directory: 'reference' },
}, {
collapsed: true, label: "Guides",
translations: { autogenerate: {
de: 'Referenz' directory: "guides",
}, },
badge: { translations: {
text: 'WIP', de: "Anleitungen",
variant: 'caution' },
} },
}] {
})] label: "More",
}); translations: {
de: "Mehr",
},
items: [
{
label: "Roadmap",
link: "/roadmap",
translations: {
de: "Zukunftspläne",
},
},
playgroundSidebarEntry
],
},
{
label: "Reference",
autogenerate: {
directory: "reference",
},
collapsed: true,
translations: {
de: "Referenz",
},
},
],
}),
],
});

View File

@ -15,6 +15,7 @@
"@astrojs/starlight": "^0.24.2", "@astrojs/starlight": "^0.24.2",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@lorenzo_lewis/starlight-utils": "^0.1.1",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@mui/icons-material": "^5.15.20", "@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20", "@mui/material": "^5.15.20",

View File

@ -20,6 +20,9 @@ dependencies:
'@emotion/styled': '@emotion/styled':
specifier: ^11.11.5 specifier: ^11.11.5
version: 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.3)(react@18.3.1) version: 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.3)(react@18.3.1)
'@lorenzo_lewis/starlight-utils':
specifier: ^0.1.1
version: 0.1.1(@astrojs/starlight@0.24.2)(astro@4.10.2)
'@monaco-editor/react': '@monaco-editor/react':
specifier: ^4.6.0 specifier: ^4.6.0
version: 4.6.0(monaco-editor@0.49.0)(react-dom@18.3.1)(react@18.3.1) version: 4.6.0(monaco-editor@0.49.0)(react-dom@18.3.1)(react@18.3.1)
@ -1410,6 +1413,19 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: false dev: false
/@lorenzo_lewis/starlight-utils@0.1.1(@astrojs/starlight@0.24.2)(astro@4.10.2):
resolution: {integrity: sha512-WBbZ9tnLxRsiiNVBzyNrANbl098/wMt7gVT09XCJtHZiiOlYQFBBYywgk/vAwzgicUe3vb27MBFc6jDOqvmu5w==}
peerDependencies:
'@astrojs/starlight': '>=0.16.0'
astro: '>=4.0.0'
dependencies:
'@astrojs/starlight': 0.24.2(astro@4.10.2)
astro: 4.10.2(sass@1.77.6)(typescript@5.4.5)
astro-integration-kit: 0.13.3(astro@4.10.2)
transitivePeerDependencies:
- '@astrojs/db'
dev: false
/@mdx-js/mdx@3.0.1: /@mdx-js/mdx@3.0.1:
resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==}
dependencies: dependencies:
@ -2161,6 +2177,13 @@ packages:
resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==}
dev: false dev: false
/ast-types@0.16.1:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
dependencies:
tslib: 2.6.3
dev: false
/astring@1.8.6: /astring@1.8.6:
resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==}
hasBin: true hasBin: true
@ -2175,6 +2198,20 @@ packages:
rehype-expressive-code: 0.35.3 rehype-expressive-code: 0.35.3
dev: false dev: false
/astro-integration-kit@0.13.3(astro@4.10.2):
resolution: {integrity: sha512-hUEQMnZ7z+7ySPCX6mXnIr0BFZU1+49eQQBg4aHjKGz1o2oZ5tvuB9Tlyj/orRH9ubd+Gkd0SSoldz0BTNe4Rg==}
peerDependencies:
'@astrojs/db': ^0.9 || ^0.10 || ^0.11
astro: ^4.4.1
peerDependenciesMeta:
'@astrojs/db':
optional: true
dependencies:
astro: 4.10.2(sass@1.77.6)(typescript@5.4.5)
pathe: 1.1.2
recast: 0.23.9
dev: false
/astro@4.10.2(sass@1.77.6)(typescript@5.4.5): /astro@4.10.2(sass@1.77.6)(typescript@5.4.5):
resolution: {integrity: sha512-SBdkoOanPsxKlKVU4uu/XG0G7NYAFoqmfBtq9SPMJ34B7Hr1MxVdEugERs8IwYN6UaxdDVcqA++9PvH6Onq2cg==} resolution: {integrity: sha512-SBdkoOanPsxKlKVU4uu/XG0G7NYAFoqmfBtq9SPMJ34B7Hr1MxVdEugERs8IwYN6UaxdDVcqA++9PvH6Onq2cg==}
engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
@ -4437,6 +4474,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: false dev: false
/pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
dev: false
/periscopic@3.1.0: /periscopic@3.1.0:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
dependencies: dependencies:
@ -4639,6 +4680,17 @@ packages:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
/recast@0.23.9:
resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==}
engines: {node: '>= 4'}
dependencies:
ast-types: 0.16.1
esprima: 4.0.1
source-map: 0.6.1
tiny-invariant: 1.3.3
tslib: 2.6.3
dev: false
/regenerator-runtime@0.14.1: /regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: false dev: false
@ -5079,6 +5131,11 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
/source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: false
/source-map@0.7.4: /source-map@0.7.4:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -5279,6 +5336,10 @@ packages:
b4a: 1.6.6 b4a: 1.6.6
dev: false dev: false
/tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
dev: false
/tm-themes@1.4.3: /tm-themes@1.4.3:
resolution: {integrity: sha512-nsUGwktLaWFMyKw2e/hNyDmcTIO+ue6Q2Mvb8L7WQkKSCflOm2TjGSspcJw6q7hi4QxQQNTuGICyvQN1UqtunQ==} resolution: {integrity: sha512-nsUGwktLaWFMyKw2e/hNyDmcTIO+ue6Q2Mvb8L7WQkKSCflOm2TjGSspcJw6q7hi4QxQQNTuGICyvQN1UqtunQ==}
dev: false dev: false
@ -5319,7 +5380,6 @@ packages:
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
requiresBuild: true requiresBuild: true
dev: false dev: false
optional: true
/tunnel-agent@0.6.0: /tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useMonaco, type Monaco } from "@monaco-editor/react"; import { useMonaco, type Monaco } from "@monaco-editor/react";
import { useImmer, type Updater } from "use-immer"; import { useImmer, type Updater } from "use-immer";
@ -14,16 +14,7 @@ import initWasm, {
compile, compile,
compileZip, compileZip,
} from "@wasm/webcompiler/pkg/webcompiler"; } from "@wasm/webcompiler/pkg/webcompiler";
import type { Directory, File, PlaygroundLang } from "@utils/playground";
export type File = {
language?: string;
content: string;
};
export type Directory = {
dirs?: { [key: string]: Directory };
files?: { [key: string]: File };
};
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
const FILE_STORAGE_KEY = "playground-files"; const FILE_STORAGE_KEY = "playground-files";
const DEFAULT_FILES = { const DEFAULT_FILES = {
@ -46,7 +37,7 @@ const DEFAULT_FILES = {
}, },
}; };
export default function Playground() { export default function Playground({ lang }: { lang: PlaygroundLang }) {
initWasm().catch((err) => { initWasm().catch((err) => {
console.error(err); console.error(err);
}); });
@ -55,32 +46,40 @@ export default function Playground() {
getStorageOrDefault(FILE_STORAGE_KEY, DEFAULT_FILES) as Directory getStorageOrDefault(FILE_STORAGE_KEY, DEFAULT_FILES) as Directory
); );
const [theme, setTheme] = useState<"light" | "dark">("dark");
const [fileName, setFileName] = useState("src/main.shu"); const [fileName, setFileName] = useState("src/main.shu");
const file = getFile(rootDir, fileName); const file = getFile(rootDir, fileName);
const onBuild = () => { const onBuild = () => {
if (monaco) { if (monaco) {
const dist = JSON.parse( const compiled = compile(getFiles(monaco));
JSON.stringify(compile(getFiles(monaco)), jsonReplacer) if (compiled) {
); const dist = JSON.parse(JSON.stringify(compiled, jsonReplacer));
const withRoot = { const withRoot = {
dirs: { dirs: {
dist: dist, dist: dist,
}, },
} as Directory; } as Directory;
loadFiles(monaco, updateRootDir, withRoot); loadFiles(monaco, updateRootDir, withRoot);
} else {
alert("Compilation failed");
}
} else { } else {
console.error("monaco has not loaded"); console.error("monaco has not loaded");
} }
}; };
const onZip = () => { const onZip = () => {
if (monaco) { if (monaco) {
const data = const zipped = compileZip(getFiles(monaco));
"data:application/zip;base64," + compileZip(getFiles(monaco)); if (zipped) {
const a = document.createElement("a"); const data = "data:application/zip;base64," + zipped;
a.href = data; const a = document.createElement("a");
a.download = "shulkerscript-pack.zip"; a.href = data;
a.click(); a.download = "shulkerscript-pack.zip";
a.click();
} else {
alert("Compilation failed");
}
} else { } else {
console.error("monaco has not loaded"); console.error("monaco has not loaded");
} }
@ -125,6 +124,37 @@ export default function Playground() {
} }
}, [monaco]); }, [monaco]);
useEffect(() => {
if (monaco) {
let isReadOnly = fileName.startsWith("dist/");
monaco.editor.getEditors().forEach((e) =>
e.updateOptions({
readOnly: isReadOnly,
readOnlyMessage: {
value: "Generated files are read-only",
},
})
);
}
}, [fileName]);
useEffect(() => {
const root = document.querySelector(":root") as HTMLElement;
if (root) {
function reactToThemeChange() {
const selectedTheme = root.getAttribute("data-theme");
if (selectedTheme !== theme && selectedTheme !== null) {
setTheme(selectedTheme as "light" | "dark");
}
}
reactToThemeChange();
root.onchange = () => {
reactToThemeChange();
};
}
});
return ( return (
<> <>
<main <main
@ -136,6 +166,7 @@ export default function Playground() {
}} }}
> >
<Header <Header
lang={lang.header}
onSave={onSave} onSave={onSave}
onReset={onReset} onReset={onReset}
onBuild={onBuild} onBuild={onBuild}
@ -146,8 +177,13 @@ export default function Playground() {
root={rootDir} root={rootDir}
fileName={fileName} fileName={fileName}
setSelectedFileName={setFileName} setSelectedFileName={setFileName}
lang={lang.explorer}
/>
<Editor
fileName={fileName}
file={file ?? undefined}
theme={theme}
/> />
<Editor fileName={fileName} file={file ?? undefined} />
</main> </main>
</> </>
); );

View File

@ -1,9 +1,17 @@
--- ---
import type { Props } from "@astrojs/starlight/props"; import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/ContentPanel.astro"; import Default from "@astrojs/starlight/components/ContentPanel.astro";
import { isPlaygroundPage } from '@utils/playground';
const isPlayground = Astro.props.slug === 'playground'; const isPlayground = isPlaygroundPage(Astro.props.slug, Astro.currentLocale);
--- ---
{
{isPlayground ? <slot /> : <Default {...Astro.props}><slot /></Default>} isPlayground ? (
<slot />
) : (
<Default {...Astro.props}>
<slot />
</Default>
)
}

View File

@ -1,8 +1,9 @@
--- ---
import type { Props } from "@astrojs/starlight/props"; import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/PageTitle.astro"; import Default from "@astrojs/starlight/components/PageTitle.astro";
import { isPlaygroundPage } from '@utils/playground';
const isPlayground = Astro.props.slug === 'playground'; const isPlayground = isPlaygroundPage(Astro.props.slug, Astro.currentLocale);
--- ---

View File

@ -1,17 +1,20 @@
import type { File } from "@components/Playground"; import type { File } from "@utils/playground";
import MonacoEditor, { useMonaco } from "@monaco-editor/react"; import MonacoEditor, { useMonaco } from "@monaco-editor/react";
import { getHighlighter, type Highlighter } from "shiki"; import { getHighlighter, type Highlighter } from "shiki";
import { shikiToMonaco } from "@shikijs/monaco"; import { shikiToMonaco } from "@shikijs/monaco";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import darkPlus from "tm-themes/themes/dark-plus.json"; import darkPlus from "tm-themes/themes/dark-plus.json";
import lightPlus from "tm-themes/themes/light-plus.json";
import { shulkerscriptGrammar } from "@utils/shulkerscript-grammar"; import { shulkerscriptGrammar } from "@utils/shulkerscript-grammar";
import { mcfunctionGrammar } from "@utils/mcfunction-grammar"; import { mcfunctionGrammar } from "@utils/mcfunction-grammar";
export default function Editor({ export default function Editor({
theme,
fileName, fileName,
file, file,
}: { }: {
theme: "light" | "dark";
fileName: string; fileName: string;
file?: File; file?: File;
}) { }) {
@ -22,9 +25,12 @@ export default function Editor({
if (monaco) { if (monaco) {
if (highlighter == null) { if (highlighter == null) {
getHighlighter({ getHighlighter({
themes: [darkPlus as any], themes: [darkPlus as any, lightPlus],
langs: ["toml", shulkerscriptGrammar, mcfunctionGrammar], langs: ["toml", shulkerscriptGrammar, mcfunctionGrammar],
}).then((highlighter) => { }).then((highlighter) => {
highlighter.setTheme(
theme === "dark" ? "dark-plus" : "light-plus"
);
setHighlighter(highlighter); setHighlighter(highlighter);
}); });
} else { } else {
@ -43,14 +49,13 @@ export default function Editor({
}, [highlighter]); }, [highlighter]);
return ( return (
<div className="editor"> <MonacoEditor
<MonacoEditor height="70vh"
height="70vh" theme={theme === "dark" ? "dark-plus" : "light-plus"}
theme="vs-dark" path={fileName}
path={fileName} defaultLanguage={file?.language}
defaultLanguage={file?.language} defaultValue={file?.content}
defaultValue={file?.content} wrapperProps={{ className: "editor" }}
/> />
</div>
); );
} }

View File

@ -1,4 +1,4 @@
import type { Directory, SetState } from "@components/Playground"; import type { Directory, PlaygroundExplorerLang, SetState } from "@utils/playground";
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
GoChevronDown as ChevDown, GoChevronDown as ChevDown,
@ -6,11 +6,13 @@ import {
} from "react-icons/go"; } from "react-icons/go";
export default function FileView({ export default function FileView({
lang,
root, root,
fileName, fileName,
setSelectedFileName, setSelectedFileName,
className, className,
}: { }: {
lang: PlaygroundExplorerLang;
root: Directory; root: Directory;
fileName: string; fileName: string;
setSelectedFileName: SetState<string>; setSelectedFileName: SetState<string>;
@ -18,7 +20,7 @@ export default function FileView({
}) { }) {
return ( return (
<div className={className}> <div className={className}>
<h3>Explorer</h3> <h3>{lang.title}</h3>
<div className="entries"> <div className="entries">
{Object.entries(root.dirs ?? {}).map(([name, dir]) => { {Object.entries(root.dirs ?? {}).map(([name, dir]) => {
return ( return (

View File

@ -1,11 +1,14 @@
import type { PlaygroundHeaderLang } from "@utils/playground";
import DropdownButton from "./DropdownButton"; import DropdownButton from "./DropdownButton";
export default function Header({ export default function Header({
lang,
onSave, onSave,
onReset, onReset,
onBuild, onBuild,
onZip, onZip,
}: { }: {
lang: PlaygroundHeaderLang;
onSave: () => void; onSave: () => void;
onReset: () => void; onReset: () => void;
onBuild: () => void; onBuild: () => void;
@ -19,17 +22,17 @@ export default function Header({
marginBottom: "0.5cm", marginBottom: "0.5cm",
}} }}
> >
<h1 id="_top">Playground</h1> <h1 id="_top">{lang.title}</h1>
<div className="buttons" style={{ height: "100%" }}> <div className="buttons" style={{ height: "100%" }}>
<DropdownButton <DropdownButton
style={{ height: "100%", marginRight: "0.5cm"}} style={{ height: "100%", marginRight: "0.5cm"}}
visible={[["Save", onSave]]} visible={[[lang.buttons.save, onSave]]}
options={[["Reset", onReset]]} options={[[lang.buttons.reset, onReset]]}
/> />
<DropdownButton <DropdownButton
style={{ height: "100%" }} style={{ height: "100%" }}
visible={[["Build", onBuild]]} visible={[[lang.buttons.build, onBuild]]}
options={[["Download zip", onZip]]} options={[[lang.buttons.zip, onZip]]}
/> />
</div> </div>
</header> </header>

View File

@ -0,0 +1,30 @@
---
import PlaygroundComponent from "@components/Playground";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import type { PlaygroundLang } from "@utils/playground";
const lang: PlaygroundLang = {
header: {
title: "Spielplatz",
buttons: {
build: "Bauen",
zip: "Als Zip herunterladen",
save: "Speichern",
reset: "Zurücksetzen",
},
},
explorer: {
title: "Dateien",
},
};
---
<StarlightPage frontmatter={{ title: "Playground", template: "splash" }}>
<PlaygroundComponent client:only="react" {lang} />
</StarlightPage>
<style is:global>
.pagination-links {
display: none;
}
</style>

View File

@ -1,8 +1,30 @@
--- ---
import PlaygroundComponent from '@components/Playground'; import PlaygroundComponent from "@components/Playground";
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import type { PlaygroundLang } from "@utils/playground";
const lang: PlaygroundLang = {
header: {
title: "Playground",
buttons: {
build: "Build",
zip: "Download zip",
save: "Save",
reset: "Reset",
},
},
explorer: {
title: "Explorer",
},
};
--- ---
<StarlightPage frontmatter={{ title: 'Playground', template: "splash" }}> <StarlightPage frontmatter={{ title: "Playground", template: "splash" }}>
<PlaygroundComponent client:only="react" /> <PlaygroundComponent client:only="react" {lang} />
</StarlightPage> </StarlightPage>
<style is:global>
.pagination-links {
display: none;
}
</style>

View File

@ -13,6 +13,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-right: 0.5cm; margin-right: 0.5cm;
overflow-x: hidden;
overflow-y: auto;
.entries { .entries {
button { button {
@ -22,15 +24,15 @@
background-color: transparent; background-color: transparent;
cursor: pointer; cursor: pointer;
&:hover {
background-color: #444444;
}
&:disabled { &:disabled {
cursor: default; cursor: default;
color: var(--sl-color-text); color: var(--sl-color-text);
background-color: #333; background-color: #333;
} }
&:hover {
background-color: #444444;
}
} }
} }
} }
@ -38,3 +40,15 @@
grid-area: editor; grid-area: editor;
} }
} }
:root[data-theme='light'] {
.playground > .file-view .entries button {
&:hover {
background-color: var(--sl-color-gray-5);
}
&:disabled {
background-color: var(--sl-color-gray-6);
}
}
}

33
src/utils/playground.ts Normal file
View File

@ -0,0 +1,33 @@
export type PlaygroundLang = {
header: PlaygroundHeaderLang;
explorer: PlaygroundExplorerLang;
};
export type PlaygroundHeaderLang = {
title: string;
buttons: {
save: string;
reset: string;
build: string;
zip: string;
};
};
export type PlaygroundExplorerLang = {
title: string;
};
export type File = {
language?: string;
content: string;
};
export type Directory = {
dirs?: { [key: string]: Directory };
files?: { [key: string]: File };
};
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
export function isPlaygroundPage(slug: string, lang?: string): boolean {
return (
slug === (!lang || lang == "en" ? "playground" : `${lang}/playground`)
);
}

View File

@ -62,7 +62,10 @@ impl From<VFolder> for Directory {
} }
for (name, item) in value.get_files() { for (name, item) in value.get_files() {
files.insert(name.to_string(), item.clone().into()); files.insert(
name.to_string(),
File::from(item.clone()).correct_lang(name),
);
} }
Self { Self {
@ -84,3 +87,16 @@ impl From<VFile> for File {
} }
} }
} }
impl File {
pub fn correct_lang(self, name: &str) -> Self {
let language = match name.split('.').last() {
Some("shu") => Some("shulkerscript".to_string()),
Some("mcfunction") => Some("mcfunction".to_string()),
Some("json" | "mcmeta") => Some("json".to_string()),
_ => None,
};
Self { language, ..self }
}
}

View File

@ -42,10 +42,10 @@ pub fn compile(root_dir: JsValue) -> JsValue {
/// Returns a base64 encoded zip file containing the compiled datapack. /// Returns a base64 encoded zip file containing the compiled datapack.
#[wasm_bindgen(js_name = compileZip)] #[wasm_bindgen(js_name = compileZip)]
pub fn compile_zip(root_dir: JsValue) -> String { pub fn compile_zip(root_dir: JsValue) -> Option<String> {
let root_dir = VFolder::from(serde_wasm_bindgen::from_value::<Directory>(root_dir).unwrap()); let root_dir = VFolder::from(serde_wasm_bindgen::from_value::<Directory>(root_dir).unwrap());
let datapack = _compile(&root_dir).unwrap(); let datapack = _compile(&root_dir).ok()?;
let mut buffer = Cursor::new(Vec::new()); let mut buffer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(&mut buffer); let mut writer = ZipWriter::new(&mut buffer);
@ -70,7 +70,7 @@ pub fn compile_zip(root_dir: JsValue) -> String {
writer.finish().unwrap(); writer.finish().unwrap();
BASE64_STANDARD.encode(buffer.into_inner()) Some(BASE64_STANDARD.encode(buffer.into_inner()))
} }
fn _compile(root_dir: &VFolder) -> Result<VFolder> { fn _compile(root_dir: &VFolder) -> Result<VFolder> {