More powerful component preview and parameter editor
This commit is contained in:
parent
ef4fa71838
commit
6e634e5d64
41
package-lock.json
generated
41
package-lock.json
generated
@ -11,17 +11,19 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
"@hotwired/turbo": "^7.1.0",
|
||||
"@hotwired/turbo": "^8.0.2",
|
||||
"@koa/router": "^12.0.1",
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@sealcode/jdd": "^0.2.4",
|
||||
"@sealcode/sealgen": "^0.11.5",
|
||||
"@sealcode/jdd": "^0.2.10",
|
||||
"@sealcode/sealgen": "^0.11.6",
|
||||
"@sealcode/ts-predicates": "^0.4.3",
|
||||
"@types/kill-port": "^2.0.0",
|
||||
"get-port": "^7.0.0",
|
||||
"js-convert-case": "^4.2.0",
|
||||
"locreq": "^3.0.0",
|
||||
"multiple-scripts-tmux": "^1.0.4",
|
||||
"nodemon": "^3.0.1",
|
||||
"object-path": "^0.11.8",
|
||||
"sealious": "^0.17.48",
|
||||
"stimulus": "^2.0.0",
|
||||
"tempstream": "^0.3.2",
|
||||
@ -31,6 +33,7 @@
|
||||
"@sealcode/ansi-html-stream": "^1.0.1",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/object-path": "^0.11.4",
|
||||
"@types/tedious": "^4.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
@ -807,9 +810,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hotwired/turbo": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.3.0.tgz",
|
||||
"integrity": "sha512-Dcu+NaSvHLT7EjrDrkEmH4qET2ZJZ5IcCWmNXxNQTBwlnE5tBZfN6WxZ842n5cHV52DH/AKNirbPBtcEXDLW4g==",
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.2.tgz",
|
||||
"integrity": "sha512-3K6QZkwWfosAV8zuM5bY+kKF02jp1lMQGsWfSE6wXdZBRBP3ah+Vj26YNqYtkEomBwRWA0QKhZgyJP7xOQkVEg==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
@ -1275,9 +1278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sealcode/jdd": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@sealcode/jdd/-/jdd-0.2.4.tgz",
|
||||
"integrity": "sha512-Lf/UIgY0N8zNHHDonvF4WQufITjWhih9+FAbb+NO21pbygrZyIaXfKPW0Vp+Eh9blTZY6QEG40H7zouuVF55ew==",
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@sealcode/jdd/-/jdd-0.2.10.tgz",
|
||||
"integrity": "sha512-8dQfskMUqotrh9Fbnk2sBcXJ12gXNM1ENPvrQOOX6VabXgE7eQc9gAZgmkcgA2prEwn1vbfpo+Lz9wxzpHOLDQ==",
|
||||
"dependencies": {
|
||||
"@sealcode/ts-predicates": "^0.5.3",
|
||||
"marked": "^12.0.0",
|
||||
@ -1301,9 +1304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sealcode/sealgen": {
|
||||
"version": "0.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.11.5.tgz",
|
||||
"integrity": "sha512-7mb8zuz2Z3KHVcVeWTRN+f4c9IVPhHKX3OzIhNv1ZY/BfkWifU5lFsBrYJUaT4Zd8EYXQIU0DAsZy4WO1miJFQ==",
|
||||
"version": "0.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.11.6.tgz",
|
||||
"integrity": "sha512-6GGZi59aia7ou2bDmejQedDNLyzfoo05bFnGVlsWXuCOMCUhBXuWGlFe3wqkSr+340iyvZiXJvBSfXg3DatX2Q==",
|
||||
"dependencies": {
|
||||
"@koa/router": "^12.0.1",
|
||||
"@sealcode/ts-predicates": "^0.4.3",
|
||||
@ -2100,6 +2103,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-1.3.4.tgz",
|
||||
"integrity": "sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA=="
|
||||
},
|
||||
"node_modules/@types/object-path": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.4.tgz",
|
||||
"integrity": "sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
|
||||
@ -8086,6 +8095,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-path": {
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
|
||||
"integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==",
|
||||
"engines": {
|
||||
"node": ">= 10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
|
@ -32,17 +32,19 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
"@hotwired/turbo": "^7.1.0",
|
||||
"@hotwired/turbo": "^8.0.2",
|
||||
"@koa/router": "^12.0.1",
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@sealcode/jdd": "^0.2.4",
|
||||
"@sealcode/sealgen": "^0.11.5",
|
||||
"@sealcode/jdd": "^0.2.10",
|
||||
"@sealcode/sealgen": "^0.11.6",
|
||||
"@sealcode/ts-predicates": "^0.4.3",
|
||||
"@types/kill-port": "^2.0.0",
|
||||
"get-port": "^7.0.0",
|
||||
"js-convert-case": "^4.2.0",
|
||||
"locreq": "^3.0.0",
|
||||
"multiple-scripts-tmux": "^1.0.4",
|
||||
"nodemon": "^3.0.1",
|
||||
"object-path": "^0.11.8",
|
||||
"sealious": "^0.17.48",
|
||||
"stimulus": "^2.0.0",
|
||||
"tempstream": "^0.3.2",
|
||||
@ -52,6 +54,7 @@
|
||||
"@sealcode/ansi-html-stream": "^1.0.1",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/object-path": "^0.11.4",
|
||||
"@types/tedious": "^4.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Templatable, tempstream } from "tempstream";
|
||||
import { FlatTemplatable, Templatable, tempstream } from "tempstream";
|
||||
import { Readable } from "stream";
|
||||
import { BaseContext } from "koa";
|
||||
import navbar from "./routes/common/navbar.js";
|
||||
import { default as default_navbar } from "./routes/common/navbar.js";
|
||||
import { toKebabCase } from "js-convert-case";
|
||||
|
||||
export const defaultHead = (ctx: BaseContext, title: string) => /* HTML */ `<title>
|
||||
${title} · ${ctx.$app.manifest.name}
|
||||
@ -10,20 +11,31 @@ export const defaultHead = (ctx: BaseContext, title: string) => /* HTML */ `<tit
|
||||
<script async src="/dist/bundle.js"></script>
|
||||
<link href="/dist/main.css" rel="stylesheet" type="text/css" />`;
|
||||
|
||||
export type HTMLOptions = {
|
||||
preserveScroll?: boolean;
|
||||
morphing?: boolean;
|
||||
navbar?: (ctx: BaseContext) => FlatTemplatable;
|
||||
};
|
||||
|
||||
export default function html(
|
||||
ctx: BaseContext,
|
||||
title: string,
|
||||
body: Templatable,
|
||||
{ preserveScroll, morphing, navbar }: HTMLOptions = {},
|
||||
makeHead: (ctx: BaseContext, title: string) => Templatable = defaultHead
|
||||
): Readable {
|
||||
ctx.set("content-type", "text/html;charset=utf-8");
|
||||
return tempstream/* HTML */ ` <!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<html lang="pl" class="title--${toKebabCase(title)}">
|
||||
<head>
|
||||
${makeHead(ctx, title)}
|
||||
${morphing ? `<meta name="turbo-refresh-method" content="morph" />` : ""}
|
||||
${preserveScroll
|
||||
? `<meta name="turbo-refresh-scroll" content="preserve">`
|
||||
: ""}
|
||||
</head>
|
||||
<body>
|
||||
${navbar(ctx)} ${body}
|
||||
${(navbar || default_navbar)(ctx)} ${body}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
@ -5,6 +5,3 @@ export const registry = new Registry();
|
||||
|
||||
import { NiceBox } from "./nice-box/nice-box.jdd.js";
|
||||
registry.add("nice-box", NiceBox);
|
||||
|
||||
import { UsingImages } from "./using-images/using-images.jdd.js";
|
||||
registry.add("using-images", UsingImages);
|
||||
|
27
src/back/routes/components.css
Normal file
27
src/back/routes/components.css
Normal file
@ -0,0 +1,27 @@
|
||||
.title--components {
|
||||
body {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 15px 1fr;
|
||||
}
|
||||
|
||||
.resize-gutter {
|
||||
background-color: gray;
|
||||
cursor: ew-resize;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resizable {
|
||||
width: var(--resizable-column-width);
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.component-preview-parameters {
|
||||
fieldset {
|
||||
background-color: #80808024;
|
||||
}
|
||||
}
|
@ -1,13 +1,53 @@
|
||||
import { TempstreamJSX, Templatable } from "tempstream";
|
||||
import { TempstreamJSX, Templatable, FlatTemplatable, tempstream } from "tempstream";
|
||||
import { BaseContext } from "koa";
|
||||
import { StatefulPage } from "@sealcode/sealgen";
|
||||
import html from "../html.js";
|
||||
import { registry } from "../jdd-components/components.js";
|
||||
import { render, simpleJDDContext } from "@sealcode/jdd";
|
||||
import {
|
||||
ComponentArgument,
|
||||
Enum,
|
||||
List,
|
||||
render,
|
||||
simpleJDDContext,
|
||||
Structured,
|
||||
} from "@sealcode/jdd";
|
||||
import objectPath from "object-path";
|
||||
|
||||
export const actionName = "Components";
|
||||
|
||||
const actions = {} as const;
|
||||
const actions = {
|
||||
add_array_item: (
|
||||
state: State,
|
||||
_: Record<string, string>,
|
||||
arg_path: string[],
|
||||
empty_value: unknown
|
||||
) => {
|
||||
const args = state.args;
|
||||
objectPath.insert(
|
||||
args,
|
||||
arg_path,
|
||||
empty_value,
|
||||
((objectPath.get(args, arg_path) as unknown[]) || []).length
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
args,
|
||||
};
|
||||
},
|
||||
remove_array_item: (
|
||||
state: State,
|
||||
_: Record<string, string>,
|
||||
arg_path: string[],
|
||||
index_to_remove: number
|
||||
) => {
|
||||
const args = state.args;
|
||||
objectPath.del(args, [...arg_path, index_to_remove]);
|
||||
return {
|
||||
...state,
|
||||
args,
|
||||
};
|
||||
},
|
||||
} as const;
|
||||
|
||||
type State = {
|
||||
component: string;
|
||||
@ -22,7 +62,150 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
||||
}
|
||||
|
||||
wrapInLayout(ctx: BaseContext, content: Templatable): Templatable {
|
||||
return html(ctx, "Components", content);
|
||||
return html(ctx, "Components", content, {
|
||||
morphing: true,
|
||||
preserveScroll: true,
|
||||
navbar: () => ``,
|
||||
});
|
||||
}
|
||||
|
||||
renderListArgument<T>(
|
||||
state: State,
|
||||
arg_path: string[],
|
||||
arg: List<ComponentArgument<T>>,
|
||||
value: T[] = []
|
||||
): FlatTemplatable {
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{arg_path.at(-1)}</legend>
|
||||
{value.map((e, i) => (
|
||||
<div style="display: flex">
|
||||
{this.renderArgumentInput(
|
||||
state,
|
||||
[...arg_path, i.toString()],
|
||||
arg.item_type,
|
||||
e
|
||||
)}
|
||||
{this.makeActionButton(
|
||||
state,
|
||||
{ action: "remove_array_item", label: "❌" },
|
||||
arg_path,
|
||||
i
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{this.makeActionButton(
|
||||
state,
|
||||
{
|
||||
action: "add_array_item",
|
||||
label: "➕",
|
||||
},
|
||||
arg_path,
|
||||
arg.item_type.getEmptyValue()
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
renderStructuredArgument<
|
||||
T extends Structured<Record<string, ComponentArgument<unknown>>>
|
||||
>(
|
||||
state: State,
|
||||
arg_path: string[],
|
||||
arg: T,
|
||||
value: Record<string, unknown>
|
||||
): FlatTemplatable {
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{arg_path.at(-1)}</legend>
|
||||
{Object.entries(arg.structure).map(([arg_name, arg]) => (
|
||||
<div>
|
||||
{this.renderArgumentInput(
|
||||
state,
|
||||
[...arg_path, arg_name],
|
||||
arg,
|
||||
(value as Record<string, unknown>)[arg_name]
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
printArgPath(path: string[]): string {
|
||||
return path.map((e) => `[${e}]`).join("");
|
||||
}
|
||||
|
||||
renderEnumArgument<T extends Enum<any>>(
|
||||
state: State,
|
||||
arg_path: string[],
|
||||
arg: T,
|
||||
value: string
|
||||
): FlatTemplatable {
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
{arg_path.at(-1)}
|
||||
<select name={`$.args${this.printArgPath(arg_path)}`}>
|
||||
{arg.values.map((v) => (
|
||||
<option value={v} selected={value == v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderArgumentInput<T>(
|
||||
state: State,
|
||||
arg_path: string[],
|
||||
arg: ComponentArgument<T>,
|
||||
value: T
|
||||
): FlatTemplatable {
|
||||
if (value === undefined) {
|
||||
value = arg.getEmptyValue();
|
||||
}
|
||||
if (arg instanceof List) {
|
||||
return this.renderListArgument(state, arg_path, arg, value as T[]);
|
||||
}
|
||||
|
||||
if (arg instanceof Structured) {
|
||||
return this.renderStructuredArgument(
|
||||
state,
|
||||
arg_path,
|
||||
arg,
|
||||
value as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
|
||||
if (arg instanceof Enum) {
|
||||
return this.renderEnumArgument(state, arg_path, arg, value as string);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
{arg_path.at(-1)}
|
||||
{arg.getTypeName() == "markdown" ? (
|
||||
<textarea
|
||||
name={`$.args${this.printArgPath(arg_path)}`}
|
||||
onblur="this.closest('form').requestSubmit()"
|
||||
cols="70"
|
||||
>
|
||||
{value as string}
|
||||
</textarea>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`$.args${this.printArgPath(arg_path)}`}
|
||||
value={value as string}
|
||||
size="70"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(ctx: BaseContext, state: State, inputs: Record<string, string>) {
|
||||
@ -30,37 +213,71 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
||||
const component =
|
||||
registry.get(state.component) || Object.values(all_components)[0];
|
||||
return (
|
||||
<div>
|
||||
<div>{JSON.stringify(state)}</div>
|
||||
<select name="$.component" onchange="this.closest('form').submit()">
|
||||
<div class="two-column">
|
||||
<div class="resizable">
|
||||
{/*The below button has to be here in order for it to be the default behavior */}
|
||||
<input type="submit" value="Preview" />
|
||||
<select
|
||||
name="$.component"
|
||||
onchange="this.closest('form').requestSubmit()"
|
||||
>
|
||||
{Object.entries(all_components).map(([name]) => (
|
||||
<option value={name} selected={name == state.component}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<fieldset>
|
||||
|
||||
<fieldset class="component-preview-parameters">
|
||||
<legend>Parameters</legend>
|
||||
{Object.entries(component.getArguments()).map(([arg_name, arg]) => (
|
||||
<div>
|
||||
<label>
|
||||
{arg_name}
|
||||
{arg.getTypeName() == "markdown" ? (
|
||||
<textarea name={`$.args[${arg_name}]`}>
|
||||
{state.args[arg_name] as string}
|
||||
</textarea>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
name={`$.args[${arg_name}]`}
|
||||
value={state.args[arg_name] as string}
|
||||
/>
|
||||
{Object.entries(component.getArguments()).map(([arg_name, arg]) =>
|
||||
this.renderArgumentInput(
|
||||
state,
|
||||
[arg_name],
|
||||
arg,
|
||||
state.args[arg_name]
|
||||
)
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
<input type="submit" value="Preview" />
|
||||
</fieldset>
|
||||
<div>{JSON.stringify(state)}</div>
|
||||
</div>
|
||||
<div class="resize-gutter"></div>
|
||||
{
|
||||
/* HTML */ `<script>
|
||||
(function () {
|
||||
let is_resizing = false;
|
||||
let origin_x;
|
||||
let origin_width;
|
||||
const gutter = document.querySelector(".resize-gutter");
|
||||
const resizable = document.querySelector(".resizable");
|
||||
const move_listener = (e) => {
|
||||
const new_width = Math.max(
|
||||
origin_width + (e.clientX - origin_x),
|
||||
1
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--resizable-column-width",
|
||||
new_width + "px"
|
||||
);
|
||||
};
|
||||
gutter.addEventListener("mousedown", (e) => {
|
||||
is_resizing = true;
|
||||
origin_x = e.clientX;
|
||||
origin_width = resizable.getBoundingClientRect().width;
|
||||
document.addEventListener("mousemove", move_listener);
|
||||
document.addEventListener("mouseup", () => {
|
||||
document.removeEventListener(
|
||||
"mousemove",
|
||||
move_listener
|
||||
);
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
})();
|
||||
</script>`
|
||||
}
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Preview</legend>
|
||||
{render(
|
||||
@ -70,6 +287,13 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// wrapInForm(state: State, content: Templatable): Templatable {
|
||||
// return tempstream/* HTML */ `<turbo-frame id="components">
|
||||
// ${super.wrapInForm(state, content)}
|
||||
// </turbo-frame> `;
|
||||
// }
|
||||
})();
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* DO NOT EDIT! This file is generated automaticaly with npx sealgen generate-scss-includes */
|
||||
/* DO NOT EDIT! This file is generated automaticaly with npx sealgen generate-css-includes */
|
||||
|
||||
@import "../node_modules/@sealcode/sealgen/src/forms/forms.css";
|
||||
@import "back/jdd-components/using-images/using-images.css";
|
||||
@import "back/routes/common/ui/input.css";
|
||||
@import "back/routes/components.css";
|
||||
@import "tables.css";
|
||||
|
Loading…
x
Reference in New Issue
Block a user