add basic code editor for playground
This commit is contained in:
parent
2842926f4f
commit
48a450c2fb
140
astro.config.mjs
140
astro.config.mjs
|
@ -2,75 +2,77 @@ import { defineConfig } from 'astro/config';
|
|||
import starlight from '@astrojs/starlight';
|
||||
import starlightLinksValidator from "starlight-links-validator";
|
||||
import shikiConfig from './src/utils/shiki';
|
||||
import react from "@astrojs/react";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'ShulkerScript',
|
||||
logo: {
|
||||
src: './src/assets/logo.webp',
|
||||
alt: 'ShulkerScript Logo',
|
||||
},
|
||||
favicon: '/favicon.ico',
|
||||
description: 'A simple and powerful scripting language for Minecraft datapacks.',
|
||||
social: {
|
||||
github: 'https://github.com/moritz-hoelting/shulkerscript-cli',
|
||||
},
|
||||
tableOfContents: { minHeadingLevel: 1, maxHeadingLevel: 3 },
|
||||
defaultLocale: 'root',
|
||||
locales: {
|
||||
root: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
},
|
||||
de: {
|
||||
label: 'Deutsch',
|
||||
lang: 'de',
|
||||
},
|
||||
},
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/moritz-hoelting/shulkerscript-webpage/edit/main',
|
||||
},
|
||||
customCss: ['./src/styles/style.css'],
|
||||
plugins: [starlightLinksValidator({
|
||||
errorOnFallbackPages: false,
|
||||
})],
|
||||
expressiveCode: {
|
||||
shiki: shikiConfig,
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Guides',
|
||||
autogenerate: {
|
||||
directory: 'guides',
|
||||
},
|
||||
translations: {
|
||||
de: 'Anleitungen',
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Roadmap',
|
||||
link: '/roadmap',
|
||||
translations: {
|
||||
de: 'Zukunftspläne',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Reference',
|
||||
autogenerate: {
|
||||
directory: 'reference',
|
||||
},
|
||||
collapsed: true,
|
||||
translations: {
|
||||
de: 'Referenz',
|
||||
},
|
||||
badge: {
|
||||
text: 'WIP',
|
||||
variant: 'caution',
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
],
|
||||
});
|
||||
integrations: [react(), starlight({
|
||||
title: 'ShulkerScript',
|
||||
logo: {
|
||||
src: './src/assets/logo.webp',
|
||||
alt: 'ShulkerScript Logo'
|
||||
},
|
||||
favicon: '/favicon.ico',
|
||||
description: 'A simple and powerful scripting language for Minecraft datapacks.',
|
||||
social: {
|
||||
github: 'https://github.com/moritz-hoelting/shulkerscript-cli'
|
||||
},
|
||||
tableOfContents: {
|
||||
minHeadingLevel: 1,
|
||||
maxHeadingLevel: 3
|
||||
},
|
||||
defaultLocale: 'root',
|
||||
locales: {
|
||||
root: {
|
||||
label: 'English',
|
||||
lang: 'en'
|
||||
},
|
||||
de: {
|
||||
label: 'Deutsch',
|
||||
lang: 'de'
|
||||
}
|
||||
},
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/moritz-hoelting/shulkerscript-webpage/edit/main'
|
||||
},
|
||||
customCss: ['./src/styles/style.css'],
|
||||
plugins: [starlightLinksValidator({
|
||||
errorOnFallbackPages: false
|
||||
})],
|
||||
expressiveCode: {
|
||||
shiki: shikiConfig
|
||||
},
|
||||
components: {
|
||||
PageTitle: './src/components/override/PageTitle.astro',
|
||||
ContentPanel: './src/components/override/ContentPanel.astro',
|
||||
},
|
||||
sidebar: [{
|
||||
label: 'Guides',
|
||||
autogenerate: {
|
||||
directory: 'guides'
|
||||
},
|
||||
translations: {
|
||||
de: 'Anleitungen'
|
||||
}
|
||||
}, {
|
||||
label: 'Roadmap',
|
||||
link: '/roadmap',
|
||||
translations: {
|
||||
de: 'Zukunftspläne'
|
||||
}
|
||||
}, {
|
||||
label: 'Reference',
|
||||
autogenerate: {
|
||||
directory: 'reference'
|
||||
},
|
||||
collapsed: true,
|
||||
translations: {
|
||||
de: 'Referenz'
|
||||
},
|
||||
badge: {
|
||||
text: 'WIP',
|
||||
variant: 'caution'
|
||||
}
|
||||
}]
|
||||
})]
|
||||
});
|
13
package.json
13
package.json
|
@ -11,10 +11,23 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.5.10",
|
||||
"@astrojs/react": "^3.6.0",
|
||||
"@astrojs/starlight": "^0.24.2",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@mui/icons-material": "^5.15.20",
|
||||
"@mui/material": "^5.15.20",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"astro": "^4.10.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sharp": "^0.32.6",
|
||||
"starlight-links-validator": "^0.7.1",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.77.6"
|
||||
}
|
||||
}
|
728
pnpm-lock.yaml
728
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,7 @@
|
|||
namespace "my-shulkerscript-pack";
|
||||
|
||||
#[tick]
|
||||
fn main() {
|
||||
// Change this
|
||||
/say Hello World!
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import Editor, { useMonaco, type Monaco } from "@monaco-editor/react";
|
||||
import FileView from "./playground/FileView";
|
||||
|
||||
import "@styles/playground.scss";
|
||||
|
||||
import mainFileContent from "@assets/playground/main.shu?raw";
|
||||
import Header from "./playground/Header";
|
||||
|
||||
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 default function Playground() {
|
||||
const rootDir: Directory = {
|
||||
dirs: {
|
||||
src: {
|
||||
files: {
|
||||
"main.shu": {
|
||||
content: mainFileContent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
"pack.toml": {
|
||||
content: "pack.toml content",
|
||||
language: "toml",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [fileName, setFileName] = useState("src/main.shu");
|
||||
const file = getFile(rootDir, fileName)!;
|
||||
|
||||
const monaco = useMonaco();
|
||||
useEffect(() => {
|
||||
if (monaco) {
|
||||
loadFiles(monaco, rootDir);
|
||||
}
|
||||
}, [monaco]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main
|
||||
className="playground not-content"
|
||||
style={{
|
||||
maxWidth: "95vw",
|
||||
marginInline: "auto",
|
||||
marginTop: "0.5cm",
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
<FileView
|
||||
className="file-view"
|
||||
root={rootDir}
|
||||
fileName={fileName}
|
||||
setSelectedFileName={setFileName}
|
||||
/>
|
||||
<div className="editor">
|
||||
<Editor
|
||||
height="60vh"
|
||||
theme="vs-dark"
|
||||
path={fileName}
|
||||
defaultLanguage={file.language}
|
||||
defaultValue={file.content}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getFile(root: Directory, path: string): File | null {
|
||||
if (path.includes("/")) {
|
||||
let dir = root;
|
||||
const split = path.split("/");
|
||||
let last = split.pop()!;
|
||||
|
||||
for (const dirName of split) {
|
||||
if (dir.dirs) {
|
||||
dir = dir.dirs[dirName];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return dir.files?.[last] ?? null;
|
||||
}
|
||||
|
||||
return root.files?.[path] ?? null;
|
||||
}
|
||||
|
||||
function loadFiles(monaco: Monaco, dir: Directory, prefix = "") {
|
||||
for (const [name, d] of Object.entries(dir.dirs ?? {})) {
|
||||
loadFiles(monaco, d, prefix + name + "/");
|
||||
}
|
||||
|
||||
for (const [name, file] of Object.entries(dir.files ?? {})) {
|
||||
loadFile(monaco, file, prefix + name);
|
||||
}
|
||||
}
|
||||
|
||||
function loadFile(monaco: Monaco, file: File, name: string) {
|
||||
const uri = monaco.Uri.parse(name);
|
||||
if (!monaco.editor.getModel(uri)) {
|
||||
monaco.editor.createModel(file.content, file.language, uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import type { Props } from "@astrojs/starlight/props";
|
||||
import Default from "@astrojs/starlight/components/ContentPanel.astro";
|
||||
|
||||
const isPlayground = Astro.props.slug === 'playground';
|
||||
---
|
||||
|
||||
|
||||
{isPlayground ? <slot /> : <Default {...Astro.props}><slot /></Default>}
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import type { Props } from "@astrojs/starlight/props";
|
||||
import Default from "@astrojs/starlight/components/PageTitle.astro";
|
||||
|
||||
const isPlayground = Astro.props.slug === 'playground';
|
||||
---
|
||||
|
||||
|
||||
{isPlayground ? <></> : <Default {...Astro.props}><slot /></Default>}
|
|
@ -0,0 +1,125 @@
|
|||
import type { Directory, SetState } from "@components/Playground";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export default function FileView({
|
||||
root,
|
||||
fileName,
|
||||
setSelectedFileName,
|
||||
className,
|
||||
}: {
|
||||
root: Directory;
|
||||
fileName: string;
|
||||
setSelectedFileName: SetState<string>;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{Object.entries(root.dirs ?? {}).map(([name, dir]) => {
|
||||
return (
|
||||
<DirElement
|
||||
key={name}
|
||||
name={name}
|
||||
dir={dir}
|
||||
fileName={fileName.slice(name.length + 1)}
|
||||
setSelectedFileName={setSelectedFileName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{Object.entries(root.files ?? {}).map(([name, _]) => {
|
||||
return (
|
||||
<span key={name}>
|
||||
<FileElement
|
||||
name={name}
|
||||
disabled={fileName == name}
|
||||
onClick={() => setSelectedFileName(name)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileElement({
|
||||
name,
|
||||
disabled,
|
||||
onClick,
|
||||
}: {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}) {
|
||||
return (
|
||||
<button disabled={disabled} onClick={onClick}>
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DirElement({
|
||||
name,
|
||||
dir: currentDir,
|
||||
collapsed: pCollapsed,
|
||||
fileName,
|
||||
setSelectedFileName,
|
||||
}: {
|
||||
name: string;
|
||||
dir: Directory;
|
||||
collapsed?: boolean;
|
||||
fileName: string;
|
||||
setSelectedFileName: SetState<string>;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(pCollapsed ?? false);
|
||||
|
||||
const modSetSelectedFileName: SetState<string> = (selected) => {
|
||||
setSelectedFileName(name + "/" + selected);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={name}>
|
||||
<button
|
||||
style={{ display: "block" }}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
{name}/
|
||||
</button>
|
||||
<div style={{ marginLeft: ".25cm" }}>
|
||||
{collapsed ? null : (
|
||||
<div>
|
||||
{Object.entries(currentDir.dirs ?? {}).map(
|
||||
([dirname, dir]) => {
|
||||
return (
|
||||
<DirElement
|
||||
key={name}
|
||||
name={name}
|
||||
dir={dir}
|
||||
fileName={fileName.slice(
|
||||
name.length + 1
|
||||
)}
|
||||
setSelectedFileName={
|
||||
modSetSelectedFileName
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
{Object.entries(currentDir.files ?? {}).map(
|
||||
([currentName, _]) => {
|
||||
return (
|
||||
<FileElement
|
||||
key={name}
|
||||
name={name}
|
||||
disabled={fileName == currentName}
|
||||
onClick={() =>
|
||||
modSetSelectedFileName(currentName)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import SplitButton from "./SplitButton";
|
||||
|
||||
export default function Header() {
|
||||
const clickBuild = () => {
|
||||
console.log("build");
|
||||
}
|
||||
const clickZip = () => {
|
||||
console.log("zip");
|
||||
}
|
||||
|
||||
return (
|
||||
<header style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "0.5cm",
|
||||
}}>
|
||||
<h1 id="_top">Playground</h1>
|
||||
<SplitButton onClick={clickBuild} options={[["Download zip", clickZip]]}>Build</SplitButton>
|
||||
</header>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import * as React from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import Grow from "@mui/material/Grow";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Popper from "@mui/material/Popper";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import MenuList from "@mui/material/MenuList";
|
||||
import { ThemeProvider } from "@mui/material";
|
||||
|
||||
import { customTheme } from "@utils/material-ui-theme";
|
||||
|
||||
export default function SplitButton({
|
||||
options,
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
options: [string, React.MouseEventHandler<HTMLLIElement>][];
|
||||
children: React.ReactNode;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMenuItemClick = (
|
||||
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
|
||||
index: number
|
||||
) => {
|
||||
options[index][1](event);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
const handleClose = (event: Event) => {
|
||||
if (
|
||||
anchorRef.current &&
|
||||
anchorRef.current.contains(event.target as HTMLElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={customTheme}>
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
ref={anchorRef}
|
||||
aria-label="Button group with a nested menu"
|
||||
>
|
||||
<Button onClick={onClick}>
|
||||
{children}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
aria-controls={open ? "split-button-menu" : undefined}
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
aria-label="select merge strategy"
|
||||
aria-haspopup="menu"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin:
|
||||
placement === "bottom"
|
||||
? "center top"
|
||||
: "center bottom",
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="split-button-menu" autoFocusItem>
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option[0]}
|
||||
onClick={(event) =>
|
||||
handleMenuItemClick(
|
||||
event,
|
||||
index
|
||||
)
|
||||
}
|
||||
>
|
||||
{option[0]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import PlaygroundComponent from '@components/Playground';
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
---
|
||||
|
||||
<StarlightPage frontmatter={{ title: 'Playground', template: "splash" }}>
|
||||
<PlaygroundComponent client:only="react" />
|
||||
</StarlightPage>
|
|
@ -0,0 +1,19 @@
|
|||
.playground {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 4fr;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"files editor";
|
||||
|
||||
> header {
|
||||
grid-area: header;
|
||||
}
|
||||
> .file-view {
|
||||
grid-area: files;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
> .editor {
|
||||
grid-area: editor;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
export const customTheme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
light: '#a700c3',
|
||||
main: '#a400c0',
|
||||
dark: '#a400c0',
|
||||
contrastText: '#fff',
|
||||
},
|
||||
secondary: {
|
||||
light: '#ff7961',
|
||||
main: '#f44336',
|
||||
dark: '#ba000d',
|
||||
contrastText: '#000',
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,3 +1,12 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@*": ["src/*"]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
},
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
Loading…
Reference in New Issue