mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-08-31 01:27:55 +00:00
Move website source/build logic to projects/web
Mostly useful as it moves some of our build logic out of the main project, as that's already pretty noisy!
This commit is contained in:
91
projects/web/build.gradle.kts
Normal file
91
projects/web/build.gradle.kts
Normal file
@@ -0,0 +1,91 @@
|
||||
plugins {
|
||||
`lifecycle-base`
|
||||
id("cc-tweaked.node")
|
||||
id("cc-tweaked.illuaminate")
|
||||
}
|
||||
|
||||
node {
|
||||
projectRoot.set(rootProject.projectDir)
|
||||
}
|
||||
|
||||
illuaminate {
|
||||
version.set(libs.versions.illuaminate)
|
||||
}
|
||||
|
||||
val rollup by tasks.registering(cc.tweaked.gradle.NpxExecToDir::class) {
|
||||
group = LifecycleBasePlugin.BUILD_GROUP
|
||||
description = "Bundles JS into rollup"
|
||||
|
||||
// Sources
|
||||
inputs.files(fileTree("src")).withPropertyName("sources")
|
||||
// Config files
|
||||
inputs.file("tsconfig.json").withPropertyName("Typescript config")
|
||||
inputs.file("rollup.config.js").withPropertyName("Rollup config")
|
||||
|
||||
// Output directory. Also defined in illuaminate.sexp and rollup.config.js
|
||||
output.set(buildDir.resolve("rollup"))
|
||||
|
||||
args = listOf("rollup", "--config", "rollup.config.js")
|
||||
}
|
||||
|
||||
val illuaminateDocs by tasks.registering(cc.tweaked.gradle.IlluaminateExecToDir::class) {
|
||||
group = JavaBasePlugin.DOCUMENTATION_GROUP
|
||||
description = "Generates docs using Illuaminate"
|
||||
|
||||
// Config files
|
||||
inputs.file(rootProject.file("illuaminate.sexp")).withPropertyName("illuaminate config")
|
||||
// Sources
|
||||
inputs.files(fileTree("doc")).withPropertyName("docs")
|
||||
inputs.files(fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom")
|
||||
inputs.files(rootProject.tasks.named("luaJavadoc"))
|
||||
// Additional assets
|
||||
inputs.files(rollup)
|
||||
inputs.file("src/styles.css").withPropertyName("styles")
|
||||
|
||||
// Output directory. Also defined in illuaminate.sexp and transform.tsx
|
||||
output.set(buildDir.resolve("illuaminate"))
|
||||
|
||||
args = listOf("doc-gen")
|
||||
workingDir = rootProject.projectDir
|
||||
}
|
||||
|
||||
val jsxDocs by tasks.registering(cc.tweaked.gradle.NpxExecToDir::class) {
|
||||
group = JavaBasePlugin.DOCUMENTATION_GROUP
|
||||
description = "Post-processes documentation to statically render some dynamic content."
|
||||
|
||||
// Config files
|
||||
inputs.file("tsconfig.json").withPropertyName("Typescript config")
|
||||
// Sources
|
||||
inputs.files(fileTree("src")).withPropertyName("sources")
|
||||
inputs.file(rootProject.file("src/generated/export/index.json")).withPropertyName("export")
|
||||
inputs.files(illuaminateDocs)
|
||||
|
||||
// Output directory. Also defined in src/transform.tsx
|
||||
output.set(buildDir.resolve("jsxDocs"))
|
||||
|
||||
args = listOf("ts-node", "-T", "--esm", "src/transform.tsx")
|
||||
}
|
||||
|
||||
val docWebsite by tasks.registering(Copy::class) {
|
||||
group = JavaBasePlugin.DOCUMENTATION_GROUP
|
||||
description = "Assemble docs and assets together into the documentation website."
|
||||
duplicatesStrategy = DuplicatesStrategy.FAIL
|
||||
|
||||
from(jsxDocs)
|
||||
|
||||
// Pick up assets from the /docs folder
|
||||
from(rootProject.file("doc")) {
|
||||
include("logo.png")
|
||||
include("images/**")
|
||||
}
|
||||
// index.js is provided by illuaminate, but rollup outputs some other chunks
|
||||
from(rollup) { exclude("index.js") }
|
||||
// Grab illuaminate's assets. HTML files are provided by jsxDocs
|
||||
from(illuaminateDocs) { exclude("**/*.html") }
|
||||
// And item/block images from the data export
|
||||
from(rootProject.file("src/generated/export/items")) { into("images/items") }
|
||||
|
||||
into(buildDir.resolve("site"))
|
||||
}
|
||||
|
||||
tasks.assemble { dependsOn(docWebsite) }
|
58
projects/web/rollup.config.js
Normal file
58
projects/web/rollup.config.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import url from '@rollup/plugin-url';
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
|
||||
const input = "src";
|
||||
const requirejs = readFileSync("../../node_modules/requirejs/require.js");
|
||||
|
||||
export default {
|
||||
input: [`${input}/index.tsx`],
|
||||
output: {
|
||||
// Also defined in build.gradle.kts
|
||||
dir: "build/rollup/",
|
||||
|
||||
// We bundle requirejs (and config) into the header. It's rather gross
|
||||
// but also works reasonably well.
|
||||
// Also suffix a ?v=${date} onto the end in the event we need to require a specific copy-cat version.
|
||||
banner: `
|
||||
${requirejs}
|
||||
require.config({
|
||||
paths: { copycat: "https://copy-cat.squiddev.cc" },
|
||||
urlArgs: function(id) { return id == "copycat/embed" ? "?v=20211221" : ""; }
|
||||
});
|
||||
`,
|
||||
format: "amd",
|
||||
preferConst: true,
|
||||
amd: {
|
||||
define: "require",
|
||||
}
|
||||
},
|
||||
context: "window",
|
||||
external: ["copycat/embed"],
|
||||
|
||||
plugins: [
|
||||
typescript(),
|
||||
|
||||
url({
|
||||
include: "**/*.dfpwm",
|
||||
fileName: "[name]-[hash][extname]",
|
||||
publicPath: "/",
|
||||
}),
|
||||
|
||||
{
|
||||
name: "cc-tweaked",
|
||||
async transform(code, file) {
|
||||
// Allow loading files in /mount.
|
||||
const ext = path.extname(file);
|
||||
return ext != '.dfpwm' && path.dirname(file) === path.resolve(`${input}/mount`)
|
||||
? `export default ${JSON.stringify(code)};\n`
|
||||
: null;
|
||||
},
|
||||
},
|
||||
|
||||
terser(),
|
||||
],
|
||||
};
|
46
projects/web/src/components/Recipe.tsx
Normal file
46
projects/web/src/components/Recipe.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import { createElement as h } from "react";
|
||||
import useExport from "./WithExport.js";
|
||||
|
||||
const Item: FunctionComponent<{ item: string }> = ({ item }) => {
|
||||
const data = useExport();
|
||||
const itemName = data.itemNames[item];
|
||||
|
||||
return <img
|
||||
src={`/images/items/${item.replace(":", "/")}.png`}
|
||||
alt={itemName}
|
||||
title={itemName}
|
||||
className="recipe-icon"
|
||||
/>
|
||||
};
|
||||
|
||||
const EmptyItem: FunctionComponent = () => <span className="recipe-icon " />;
|
||||
|
||||
const Arrow: FunctionComponent<JSX.IntrinsicElements["svg"]> = (props) => <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.513 45.512" {...props}>
|
||||
<g>
|
||||
<path d="M44.275,19.739L30.211,5.675c-0.909-0.909-2.275-1.18-3.463-0.687c-1.188,0.493-1.959,1.654-1.956,2.938l0.015,5.903
|
||||
l-21.64,0.054C1.414,13.887-0.004,15.312,0,17.065l0.028,11.522c0.002,0.842,0.338,1.648,0.935,2.242s1.405,0.927,2.247,0.925
|
||||
l21.64-0.054l0.014,5.899c0.004,1.286,0.781,2.442,1.971,2.931c1.189,0.487,2.557,0.21,3.46-0.703L44.29,25.694
|
||||
C45.926,24.043,45.92,21.381,44.275,19.739z" fill="var(--recipe-hover)" />
|
||||
</g>
|
||||
</svg>;
|
||||
|
||||
const Recipe: FunctionComponent<{ recipe: string }> = ({ recipe }) => {
|
||||
const data = useExport();
|
||||
const recipeInfo = data.recipes[recipe];
|
||||
if (!recipeInfo) throw Error("Cannot find recipe for " + recipe);
|
||||
|
||||
return <div className="recipe">
|
||||
<strong className="recipe-title">{data.itemNames[recipeInfo.output]}</strong>
|
||||
<div className="recipe-inputs">
|
||||
{recipeInfo.inputs.map((items, i) => <div className="recipe-item recipe-input" key={i}>{items ? <Item item={items[0]} /> : <EmptyItem />}</div>)}
|
||||
</div>
|
||||
<Arrow className="recipe-arrow" />
|
||||
<div className="recipe-item recipe-output">
|
||||
<Item item={recipeInfo.output} />
|
||||
{recipeInfo.count > 1 && <span className="recipe-count">{recipeInfo.count}</span>}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default Recipe;
|
23
projects/web/src/components/WithExport.tsx
Normal file
23
projects/web/src/components/WithExport.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createElement as h, useContext, createContext, FunctionComponent, ReactNode } from "react";
|
||||
|
||||
export type DataExport = {
|
||||
readonly itemNames: Record<string, string>,
|
||||
readonly recipes: Record<string, Recipe>,
|
||||
};
|
||||
|
||||
export type Recipe = {
|
||||
readonly inputs: Array<Array<string>>,
|
||||
readonly output: string,
|
||||
readonly count: number,
|
||||
};
|
||||
|
||||
const DataExport = createContext<DataExport>({
|
||||
itemNames: {},
|
||||
recipes: {},
|
||||
});
|
||||
|
||||
export const useExport = () => useContext(DataExport);
|
||||
export default useExport;
|
||||
|
||||
export const WithExport: FunctionComponent<{ data: DataExport, children: ReactNode }> =
|
||||
({ data, children }) => <DataExport.Provider value={data}> {children}</DataExport.Provider >;
|
25
projects/web/src/components/support.tsx
Normal file
25
projects/web/src/components/support.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
|
||||
/**
|
||||
* Wrap a component and ensure that no children are passed to it.
|
||||
*
|
||||
* Our custom tags *must* be explicitly closed, as <foo /> will be parsed as
|
||||
* <foo>(rest of the document)</foo>. This ensures you've not forgotten to do
|
||||
* that.
|
||||
*
|
||||
* @param component The component to wrap
|
||||
* @returns A new functional component identical to the previous one
|
||||
*/
|
||||
export const noChildren = function <T>(component: FunctionComponent<T>): FunctionComponent<T> {
|
||||
// I hope that our few remaining friends
|
||||
// Give up on trying to save us
|
||||
|
||||
const name = component.displayName ?? component.name;
|
||||
const wrapped: FunctionComponent<T> = props => {
|
||||
if ((props as any).children) throw Error("Unexpected children in " + name);
|
||||
|
||||
return component(props);
|
||||
};
|
||||
wrapped.displayName = name;
|
||||
return wrapped;
|
||||
}
|
206
projects/web/src/index.tsx
Normal file
206
projects/web/src/index.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { render, h, Component, Computer, PeripheralKind } from "copycat/embed";
|
||||
import type { ComponentChild } from "preact";
|
||||
|
||||
import settingsFile from "./mount/.settings";
|
||||
import startupFile from "./mount/startup.lua";
|
||||
import exprTemplate from "./mount/expr_template.lua";
|
||||
import exampleNfp from "./mount/example.nfp";
|
||||
import exampleNft from "./mount/example.nft";
|
||||
import exampleAudioLicense from "./mount/example.dfpwm.LICENSE";
|
||||
import exampleAudioUrl from "./mount/example.dfpwm";
|
||||
|
||||
const defaultFiles: { [filename: string]: string } = {
|
||||
".settings": settingsFile,
|
||||
"startup.lua": startupFile,
|
||||
|
||||
"data/example.nfp": exampleNfp,
|
||||
"data/example.nft": exampleNft,
|
||||
};
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number => {
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
const download = async (url: string): Promise<Uint8Array> => {
|
||||
const result = await fetch(url);
|
||||
if (result.status != 200) throw new Error(`${url} responded with ${result.status} ${result.statusText}`);
|
||||
|
||||
return new Uint8Array(await result.arrayBuffer());
|
||||
};
|
||||
|
||||
let dfpwmAudio: Promise<Uint8Array> | null = null;
|
||||
|
||||
const Click = (options: { run: () => void }) =>
|
||||
<button type="button" class="example-run" onClick={options.run}>Run ᐅ</button>
|
||||
|
||||
type WindowProps = {};
|
||||
|
||||
type Example = {
|
||||
files: { [file: string]: string | Uint8Array },
|
||||
peripheral: PeripheralKind | null,
|
||||
}
|
||||
|
||||
type WindowState = {
|
||||
exampleIdx: number,
|
||||
} & ({ visible: false, example: null } | { visible: true, example: Example })
|
||||
|
||||
type Touch = { clientX: number, clientY: number };
|
||||
|
||||
class Window extends Component<WindowProps, WindowState> {
|
||||
private positioned: boolean = false;
|
||||
private left: number = 0;
|
||||
private top: number = 0;
|
||||
private dragging?: { downX: number, downY: number, initialX: number, initialY: number };
|
||||
|
||||
private snippets: { [file: string]: string } = {};
|
||||
|
||||
constructor(props: WindowProps, context: unknown) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
visible: false,
|
||||
example: null,
|
||||
exampleIdx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const elements = document.querySelectorAll("pre[data-lua-kind]");
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i] as HTMLElement;
|
||||
|
||||
let example = element.innerText;
|
||||
|
||||
const snippet = element.getAttribute("data-snippet");
|
||||
if (snippet) this.snippets[snippet] = example;
|
||||
|
||||
// We attempt to pretty-print the result of a function _except_ when the function
|
||||
// is print. This is pretty ugly, but prevents the confusing trailing "1".
|
||||
if (element.getAttribute("data-lua-kind") == "expr" && !example.startsWith("print(")) {
|
||||
example = exprTemplate.replace("__expr__", example);
|
||||
}
|
||||
|
||||
const mount = element.getAttribute("data-mount");
|
||||
const peripheral = element.getAttribute("data-peripheral");
|
||||
render(<Click run={this.runExample(example, mount, peripheral)} />, element);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(_: WindowProps, { visible }: WindowState) {
|
||||
if (!visible && this.state.visible) this.setPosition(this.left, this.top);
|
||||
}
|
||||
|
||||
public render(_: WindowProps, { visible, example, exampleIdx }: WindowState): ComponentChild {
|
||||
return visible ? <div class="example-window" style={`transform: translate(${this.left}px, ${this.top}px);`}>
|
||||
<div class="titlebar">
|
||||
<div class="titlebar-drag" onMouseDown={this.onMouseDown} onTouchStart={this.onTouchDown} />
|
||||
<button type="button" class="titlebar-close" onClick={this.close}>{"\u2715"}</button>
|
||||
</div>
|
||||
<div class="computer-container">
|
||||
<Computer key={exampleIdx} files={{
|
||||
...defaultFiles, ...example!.files,
|
||||
}} peripherals={{ back: example!.peripheral }} />
|
||||
</div>
|
||||
</div> : <div class="example-window example-window-hidden" />;
|
||||
}
|
||||
|
||||
private runExample(example: string, mount: string | null, peripheral: string | null): () => void {
|
||||
return async () => {
|
||||
if (!this.positioned) {
|
||||
this.positioned = true;
|
||||
this.left = 20;
|
||||
this.top = 20;
|
||||
}
|
||||
|
||||
const files: { [file: string]: string | Uint8Array } = { "example.lua": example };
|
||||
if (mount !== null) {
|
||||
for (const toMount of mount.split(",")) {
|
||||
const [name, path] = toMount.split(":", 2);
|
||||
files[path] = this.snippets[name] || "";
|
||||
}
|
||||
}
|
||||
|
||||
if (example.includes("data/example.dfpwm")) {
|
||||
files["data/example.dfpwm.LICENSE"] = exampleAudioLicense;
|
||||
|
||||
try {
|
||||
if (dfpwmAudio === null) dfpwmAudio = download(exampleAudioUrl);
|
||||
files["data/example.dfpwm"] = await dfpwmAudio;
|
||||
} catch (e) {
|
||||
console.error("Cannot download example dfpwm", e);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(({ exampleIdx }: WindowState) => ({
|
||||
visible: true,
|
||||
example: {
|
||||
files,
|
||||
peripheral: peripheral as PeripheralKind | null,
|
||||
},
|
||||
exampleIdx: exampleIdx + 1,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly close = () => this.setState({ visible: false });
|
||||
|
||||
// All the dragging code is terrible. However, I've had massive performance
|
||||
// issues doing it other ways, so this'll have to do.
|
||||
private onDown(e: Event, touch: Touch) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.dragging = {
|
||||
initialX: this.left, initialY: this.top,
|
||||
downX: touch.clientX, downY: touch.clientY
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", this.onMouseDrag, true);
|
||||
window.addEventListener("touchmove", this.onTouchDrag, true);
|
||||
window.addEventListener("mouseup", this.onUp, true);
|
||||
window.addEventListener("touchend", this.onUp, true);
|
||||
}
|
||||
private readonly onMouseDown = (e: MouseEvent) => this.onDown(e, e);
|
||||
private readonly onTouchDown = (e: TouchEvent) => this.onDown(e, e.touches[0]);
|
||||
|
||||
private onDrag(e: Event, touch: Touch) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const dragging = this.dragging;
|
||||
if (!dragging) return;
|
||||
|
||||
this.setPosition(
|
||||
dragging.initialX + (touch.clientX - dragging.downX),
|
||||
dragging.initialY + (touch.clientY - dragging.downY),
|
||||
);
|
||||
};
|
||||
private readonly onMouseDrag = (e: MouseEvent) => this.onDrag(e, e);
|
||||
private readonly onTouchDrag = (e: TouchEvent) => this.onDrag(e, e.touches[0]);
|
||||
|
||||
private readonly onUp = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
|
||||
this.dragging = undefined;
|
||||
|
||||
window.removeEventListener("mousemove", this.onMouseDrag, true);
|
||||
window.removeEventListener("touchmove", this.onTouchDrag, true);
|
||||
window.removeEventListener("mouseup", this.onUp, true);
|
||||
window.removeEventListener("touchend", this.onUp, true);
|
||||
}
|
||||
|
||||
private readonly setPosition = (left: number, top: number): void => {
|
||||
const root = this.base as HTMLElement;
|
||||
|
||||
left = this.left = clamp(left, 0, window.innerWidth - root.offsetWidth);
|
||||
top = this.top = clamp(top, 0, window.innerHeight - root.offsetHeight);
|
||||
root.style.transform = `translate(${left}px, ${top}px)`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const root = document.createElement("div");
|
||||
document.body.appendChild(root);
|
||||
render(<Window />, document.body, root);
|
3
projects/web/src/mount/.settings
Normal file
3
projects/web/src/mount/.settings
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
[ "motd.enable" ] = false,
|
||||
}
|
2982
projects/web/src/mount/example.dfpwm
Normal file
2982
projects/web/src/mount/example.dfpwm
Normal file
File diff suppressed because one or more lines are too long
3
projects/web/src/mount/example.dfpwm.LICENSE
Normal file
3
projects/web/src/mount/example.dfpwm.LICENSE
Normal file
@@ -0,0 +1,3 @@
|
||||
Playing Soliloquy [Remake] by Alcakight
|
||||
Source: https://soundcloud.com/alcaknight/soliloquy-remake
|
||||
License: under CC BY 3.0
|
16
projects/web/src/mount/example.nfp
Normal file
16
projects/web/src/mount/example.nfp
Normal file
@@ -0,0 +1,16 @@
|
||||
fffffffffffffffffffffff
|
||||
f444444444444444444444f
|
||||
f444444444444444444444f
|
||||
f44fffffffffffffffff44f
|
||||
f44ff0ffffffffffffff44f
|
||||
f44fff0fffffffffffff44f
|
||||
f44ff0ffffffffffffff44f
|
||||
f44fffffffffffffffff44f
|
||||
f44fffffffffffffffff44f
|
||||
f44fffffffffffffffff44f
|
||||
f44fffffffffffffffff44f
|
||||
f44fffffffffffffffff44f
|
||||
f444444444444444444444f
|
||||
f4444444444444444fff44f
|
||||
f444444444444444444444f
|
||||
fffffffffffffffffffffff
|
15
projects/web/src/mount/example.nft
Normal file
15
projects/web/src/mount/example.nft
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
4
|
||||
4
|
||||
4 4
|
||||
0 4 4>0 ls 4
|
||||
0 4 drom/ 4 0
|
||||
0 4 startup.lua 4
|
||||
0 4 4> 0hello 4
|
||||
0 4 aHello World! 0 4
|
||||
0 4 4
|
||||
0 4 4
|
||||
0 4 4
|
||||
0 4
|
||||
0 4 4
|
||||
0 4
|
14
projects/web/src/mount/expr_template.lua
Normal file
14
projects/web/src/mount/expr_template.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local result = table.pack(__expr__
|
||||
)
|
||||
|
||||
if result.n == 0 then return end
|
||||
|
||||
local pp = require "cc.pretty"
|
||||
|
||||
local line = {}
|
||||
for i = 1, result.n do
|
||||
if i > 1 then line[#line + 1] = pp.text(", ") end
|
||||
line[#line + 1] = pp.pretty(result[i])
|
||||
end
|
||||
|
||||
pp.print(pp.concat(table.unpack(line)))
|
14
projects/web/src/mount/startup.lua
Normal file
14
projects/web/src/mount/startup.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Print out license information if needed
|
||||
if fs.exists("data/example.dfpwm") then
|
||||
local h = io.open("data/example.dfpwm.LICENSE")
|
||||
local contents = h:read("*a")
|
||||
h:close()
|
||||
|
||||
write(contents)
|
||||
end
|
||||
|
||||
-- Make the startup file invisible, then run the file. We could use
|
||||
-- shell.run, but this ensures the program is in shell history, etc...
|
||||
fs.delete("startup.lua")
|
||||
os.queueEvent("paste", "example.lua")
|
||||
os.queueEvent("key", keys.enter, false)
|
180
projects/web/src/styles.css
Normal file
180
projects/web/src/styles.css
Normal file
@@ -0,0 +1,180 @@
|
||||
:root {
|
||||
--nav-width: 250px;
|
||||
}
|
||||
/* Some misc styles */
|
||||
|
||||
.big-image {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
/* Pretty tables, mostly inherited from table.definition-list */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
border: 1px solid #cccccc;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: var(--background-2);
|
||||
}
|
||||
|
||||
pre.highlight {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.example-run {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: #058e05;
|
||||
color: #fff;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.example-window {
|
||||
position: fixed;
|
||||
z-index: 200;
|
||||
top: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
/* Behold, the most cursed CSS! copy-cat's resizing algorithm is a weird, in
|
||||
that it basically does "wrapper height - 40px" to determine the effective
|
||||
size. But the footer is actually 1em+6px high, so we need to do very weird
|
||||
things.
|
||||
|
||||
Yes, it should probably be fixed on the copy-cat side.
|
||||
*/
|
||||
.computer-container {
|
||||
width: 620px;
|
||||
height: calc(350px + 40px);
|
||||
margin-top: calc((1em + 6px - 40px) / 2);
|
||||
}
|
||||
|
||||
.example-window-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
display: flex;
|
||||
background: #dede6c;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.titlebar-drag {
|
||||
flex-grow: 1;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.titlebar-close {
|
||||
background: none;
|
||||
padding: 0px 5px;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
margin: 0px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.titlebar-close:hover {
|
||||
background: #cc4c4c;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.computer-container {
|
||||
width: 314px;
|
||||
height: calc(179px + 40px);
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.titlebar-close {
|
||||
font-size: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--recipe-bg: #ddd;
|
||||
--recipe-fg: #bbb;
|
||||
--recipe-hover: #aaa;
|
||||
|
||||
--recipe-padding: 4px;
|
||||
--recipe-size: 32px;
|
||||
}
|
||||
|
||||
.recipe-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 1em 0;
|
||||
gap: 1em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.recipe {
|
||||
display: inline-grid;
|
||||
padding: var(--recipe-padding);
|
||||
|
||||
background: var(--recipe-bg);
|
||||
column-gap: var(--recipe-padding);
|
||||
row-gap: var(--recipe-padding);
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
color: #222; /* Same as --foreground in light theme. Ugly, but too lazy to style in dark for now. */
|
||||
grid-column-start: span 3;
|
||||
}
|
||||
|
||||
.recipe-inputs {
|
||||
display: inline-grid;
|
||||
column-gap: var(--recipe-padding);
|
||||
row-gap: var(--recipe-padding);
|
||||
align-items: center;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.recipe-item {
|
||||
padding: var(--recipe-padding);
|
||||
background-color: var(--recipe-fg);
|
||||
}
|
||||
|
||||
.recipe-item:hover {
|
||||
background-color: var(--recipe-hover);
|
||||
}
|
||||
|
||||
.recipe-icon {
|
||||
display: block;
|
||||
width: var(--recipe-size);
|
||||
height: var(--recipe-size);
|
||||
max-width: 10vw;
|
||||
max-height: 10vw;
|
||||
}
|
||||
|
||||
.recipe-arrow {
|
||||
width: var(--recipe-size);
|
||||
max-width: 10vw;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.recipe-output {
|
||||
align-self: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.recipe-count {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: var(--recipe-padding);
|
||||
color: #fff;
|
||||
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000
|
||||
}
|
53
projects/web/src/transform.tsx
Normal file
53
projects/web/src/transform.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Find all HTML files generated by illuaminate and pipe them through a remark.
|
||||
*
|
||||
* This performs compile-time syntax highlighting and expands our custom
|
||||
* components using React SSR.
|
||||
*
|
||||
* Yes, this would be so much nicer with next.js.
|
||||
*/
|
||||
import * as fs from "fs/promises";
|
||||
import globModule from "glob";
|
||||
import * as path from "path";
|
||||
import { createElement, createElement as h, Fragment } from 'react';
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import rehypeParse from 'rehype-parse';
|
||||
import rehypeReact from 'rehype-react';
|
||||
import { unified } from 'unified';
|
||||
import { promisify } from "util";
|
||||
// Our components
|
||||
import Recipe from "./components/Recipe.js";
|
||||
import { noChildren } from "./components/support.js";
|
||||
import { DataExport, WithExport } from "./components/WithExport.js";
|
||||
|
||||
const glob = promisify(globModule);
|
||||
|
||||
(async () => {
|
||||
const base = "build/illuaminate";
|
||||
|
||||
const processor = unified()
|
||||
.use(rehypeParse, { emitParseErrors: true })
|
||||
.use(rehypeHighlight, { prefix: "" })
|
||||
.use(rehypeReact, {
|
||||
createElement,
|
||||
Fragment,
|
||||
passNode: false,
|
||||
components: {
|
||||
['mc-recipe']: noChildren(Recipe),
|
||||
['mcrecipe']: noChildren(Recipe),
|
||||
} as any
|
||||
});
|
||||
|
||||
const dataExport = JSON.parse(await fs.readFile("../../src/generated/export/index.json", "utf-8")) as DataExport;
|
||||
|
||||
for (const file of await glob(base + "/**/*.html")) {
|
||||
const contents = await fs.readFile(file, "utf-8");
|
||||
|
||||
const { result } = await processor.process(contents);
|
||||
|
||||
const outputPath = path.resolve("build/jsxDocs", path.relative(base, file));
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, "<!doctype HTML>" + renderToStaticMarkup(<WithExport data={dataExport}>{result}</WithExport>));
|
||||
}
|
||||
})();
|
61
projects/web/src/typings.ts
Normal file
61
projects/web/src/typings.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
declare module "*.lua" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
declare module "*.nfp" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
declare module "*.nft" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
|
||||
declare module "*.settings" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
declare module "*.LICENSE" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
declare module "*.dfpwm" {
|
||||
const contents: string;
|
||||
export default contents;
|
||||
}
|
||||
|
||||
|
||||
declare module "copycat/embed" {
|
||||
import { h, Component, render, ComponentChild } from "preact";
|
||||
|
||||
export type Side = "up" | "down" | "left" | "right" | "front" | "back";
|
||||
export type PeripheralKind = "speaker";
|
||||
|
||||
export { h, Component, render };
|
||||
|
||||
export type ComputerAccess = unknown;
|
||||
|
||||
export type MainProps = {
|
||||
hdFont?: boolean | string,
|
||||
persistId?: number,
|
||||
files?: { [filename: string]: string | ArrayBuffer },
|
||||
label?: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
resolve?: (computer: ComputerAccess) => void,
|
||||
peripherals?: {
|
||||
[side in Side]?: PeripheralKind | null
|
||||
},
|
||||
}
|
||||
|
||||
class Computer extends Component<MainProps, unknown> {
|
||||
public render(props: MainProps, state: unknown): ComponentChild;
|
||||
}
|
||||
|
||||
export { Computer };
|
||||
}
|
32
projects/web/tsconfig.json
Normal file
32
projects/web/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esNext",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"es2015",
|
||||
"dom"
|
||||
],
|
||||
"newLine": "LF",
|
||||
"baseUrl": ".",
|
||||
// Additional compile options
|
||||
"noEmitOnError": true,
|
||||
"preserveWatchOutput": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
// Strict Type-Checking Options
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
// Needed for some of our internal stuff.
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user