Enable EarlyAssets and stimulus controllers support. Improve layout in component debugger

This commit is contained in:
Kuba Orlik 2024-03-30 15:14:52 +01:00
parent c654b092ee
commit 148c533b5b
20 changed files with 454 additions and 336 deletions

29
package-lock.json generated
View File

@ -14,10 +14,11 @@
"@hotwired/turbo": "^8.0.2", "@hotwired/turbo": "^8.0.2",
"@koa/router": "^12.0.1", "@koa/router": "^12.0.1",
"@playwright/test": "^1.36.1", "@playwright/test": "^1.36.1",
"@sealcode/jdd": "^0.3.3", "@sealcode/jdd": "^0.3.4",
"@sealcode/sealgen": "^0.14.4", "@sealcode/sealgen": "^0.14.5",
"@sealcode/ts-predicates": "^0.4.3", "@sealcode/ts-predicates": "^0.4.3",
"@types/kill-port": "^2.0.0", "@types/kill-port": "^2.0.0",
"@types/leaflet": "^1.9.8",
"get-port": "^7.0.0", "get-port": "^7.0.0",
"js-convert-case": "^4.2.0", "js-convert-case": "^4.2.0",
"koa-responsive-image-router": "^0.2.19", "koa-responsive-image-router": "^0.2.19",
@ -791,9 +792,9 @@
} }
}, },
"node_modules/@sealcode/jdd": { "node_modules/@sealcode/jdd": {
"version": "0.3.3", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/@sealcode/jdd/-/jdd-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@sealcode/jdd/-/jdd-0.3.4.tgz",
"integrity": "sha512-mGOBlE/gQSSdXZfg5wP6mq8vw6q/qrxB6HIMGvctSZLAAFjiz/ExnOLN+5lIGmO5XpNRWcCHHlBmFKpBy4Vz3A==", "integrity": "sha512-lwkVoUguOsOxuKYURw/hl3XqFARY85CuqnwzgdHG+gC6x23y/PgljbPREAT5GSKYCIVWkwGdNAfbh0HglV4Z4Q==",
"dependencies": { "dependencies": {
"@sealcode/ts-predicates": "^0.5.3", "@sealcode/ts-predicates": "^0.5.3",
"koa-responsive-image-router": "^0.2.19", "koa-responsive-image-router": "^0.2.19",
@ -820,8 +821,9 @@
} }
}, },
"node_modules/@sealcode/sealgen": { "node_modules/@sealcode/sealgen": {
"version": "0.14.4", "version": "0.14.5",
"license": "ISC", "resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.14.5.tgz",
"integrity": "sha512-SE3dpS5KAsvpznKJla1VoqlnDNxeHJ2mTN2Kzdwg9Chleht/Mw/ojb7E58ZX/WxJNg+BRuCYy6knJkvkO7SfQg==",
"dependencies": { "dependencies": {
"@koa/router": "^12.0.1", "@koa/router": "^12.0.1",
"@sealcode/ts-predicates": "^0.4.3", "@sealcode/ts-predicates": "^0.4.3",
@ -1031,6 +1033,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/geojson": {
"version": "7946.0.14",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="
},
"node_modules/@types/http-assert": { "node_modules/@types/http-assert": {
"version": "1.5.5", "version": "1.5.5",
"license": "MIT" "license": "MIT"
@ -1119,6 +1126,14 @@
"@types/koa-send": "*" "@types/koa-send": "*"
} }
}, },
"node_modules/@types/leaflet": {
"version": "1.9.8",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.8.tgz",
"integrity": "sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.3.8", "version": "3.3.8",
"license": "MIT" "license": "MIT"

View File

@ -6,8 +6,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "docker-compose up -d db && node .", "start": "docker-compose up -d db && node .",
"typecheck:back": "npx tsc --noEmit --target es6 --lib es2021,dom -p src/back", "typecheck:back": "npx tsc --noEmit --target es6 --lib es2021,dom -p tsconfig-back.json",
"typecheck:front": "npx tsc --noEmit --target es6 --lib es2015,dom -p src/front", "typecheck:front": "npx tsc --noEmit --target es6 --lib es2015,dom -p tsconfig-front.json",
"build": "sealgen build", "build": "sealgen build",
"watch": "multiple-scripts-tmux -p watch", "watch": "multiple-scripts-tmux -p watch",
"reset-db": "docker-compose down && docker-compose up -d", "reset-db": "docker-compose down && docker-compose up -d",
@ -35,10 +35,11 @@
"@hotwired/turbo": "^8.0.2", "@hotwired/turbo": "^8.0.2",
"@koa/router": "^12.0.1", "@koa/router": "^12.0.1",
"@playwright/test": "^1.36.1", "@playwright/test": "^1.36.1",
"@sealcode/jdd": "^0.3.3", "@sealcode/jdd": "^0.3.4",
"@sealcode/sealgen": "^0.14.4", "@sealcode/sealgen": "^0.14.5",
"@sealcode/ts-predicates": "^0.4.3", "@sealcode/ts-predicates": "^0.4.3",
"@types/kill-port": "^2.0.0", "@types/kill-port": "^2.0.0",
"@types/leaflet": "^1.9.8",
"get-port": "^7.0.0", "get-port": "^7.0.0",
"js-convert-case": "^4.2.0", "js-convert-case": "^4.2.0",
"koa-responsive-image-router": "^0.2.19", "koa-responsive-image-router": "^0.2.19",

View File

@ -46,6 +46,11 @@ export default function html(
) => Templatable = defaultHead ) => Templatable = defaultHead
): Readable { ): Readable {
ctx.set("content-type", "text/html;charset=utf-8"); ctx.set("content-type", "text/html;charset=utf-8");
const controllers: string[] = [];
if (htmlOptions.autoRefreshCSS) {
controllers.push("refresh-styles");
controllers.push("refresh-on-ts-changes");
}
return tempstream/* HTML */ ` <!DOCTYPE html> return tempstream/* HTML */ ` <!DOCTYPE html>
<html <html
lang="${htmlOptions.language || DEFAULT_HTML_LANG}" lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
@ -54,146 +59,8 @@ export default function html(
<head> <head>
${makeHead(ctx, title, htmlOptions)} ${makeHead(ctx, title, htmlOptions)}
</head> </head>
<body> <body data-controller="${controllers.join(" ")}">
${(htmlOptions.navbar || default_navbar)(ctx)} ${body} ${(htmlOptions.navbar || default_navbar)(ctx)} ${body}
${htmlOptions.autoRefreshCSS
? /* HTML */ `<script>
function make_new_link() {
const new_link = document.createElement("link");
new_link.rel = "stylesheet";
new_link.href = \`/dist/main.css?\${Math.random()}+\${Math.random()}\`;
new_link.type = "text/css";
return new_link;
}
function getStyles() {
return Array.from(
document.querySelectorAll("head link")
).filter(
(e) => new URL(e.href).pathname == "/dist/main.css"
);
}
function cleanup_css() {
console.log("clearing styles");
getStyles()
.slice(0, -1)
.forEach((style) => {
style.parentElement.removeChild(style);
});
}
document.documentElement.addEventListener(
"turbo:morph",
cleanup_css
);
const sleep = (time) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});
const APP_DOWN_ERROR_MESSAGE = "App is currently down";
function get_status() {
return fetch("/status.json").then((r) => r.json());
}
async function wait_for_run_id_to_change() {
let first_timestamp;
try {
const { started_at, status } = await get_status();
first_timestamp = started_at;
} catch (e) {
await wait_for_app_to_be_stable();
return;
}
if (!first_timestamp) {
throw new Error(APP_DOWN_ERROR_MESSAGE);
}
while (true) {
const { started_at, status } =
await get_status().catch(() => ({
started_at: first_timestamp,
}));
if (started_at !== first_timestamp) {
return;
}
await sleep(100);
}
}
async function wait_for_app_to_be_stable(n = 3) {
console.log("Waiting for app to be stable....");
let counter = 0;
while (true) {
const { status } = await get_status().catch((e) => ({
status: "down",
}));
if (status == "running") {
console.log(counter);
counter++;
} else {
counter = 0;
}
if (counter == n) {
return;
}
await sleep(100);
}
}
async function wait_for_app_restart() {
try {
await wait_for_run_id_to_change();
} catch (e) {
if (e.message !== APP_DOWN_ERROR_MESSAGE) {
throw e;
}
}
await wait_for_app_to_be_stable();
}
(async function () {
const { started_at, status } = await fetch(
"/status.json"
).then((r) => r.json());
last_known_start_timestamp = started_at;
const { port, watch } = await fetch(
"/dist/notifier.json"
).then((r) => r.json());
if (!watch) {
console.warning(
"Not running auto refresh on watch because the build process is not running in watch mode"
);
return;
}
const socket = new WebSocket(\`ws://localhost:\${port}\`);
socket.onmessage = async (message) => {
if (message.data === "css") {
const new_link = make_new_link();
new_link.onload = cleanup_css;
document
.querySelector("head")
.appendChild(new_link);
}
if (message.data === "ts") {
document.documentElement.classList.add(
"restarting"
);
await wait_for_app_restart();
document.documentElement.dispatchEvent(
new Event("ts-rebuilt")
);
document.documentElement.classList.remove(
"restarting"
);
}
};
})();
</script>`
: ""}
${htmlOptions.disableCopyEvent ${htmlOptions.disableCopyEvent
? /* HTML */ "<script>document.addEventListener('copy', (e) => e.preventDefault());</script>" ? /* HTML */ "<script>document.addEventListener('copy', (e) => e.preventDefault());</script>"
: ""} : ""}

View File

@ -0,0 +1,96 @@
import { Controller } from "stimulus";
const APP_DOWN_ERROR_MESSAGE = "App is currently down";
const sleep = (time: number) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});
async function get_status() {
const r = await fetch("/status.json");
return await r.json();
}
async function wait_for_run_id_to_change() {
let first_timestamp: number;
try {
const { started_at } = await get_status();
first_timestamp = started_at;
} catch (e) {
await wait_for_app_to_be_stable();
return;
}
if (!first_timestamp) {
throw new Error(APP_DOWN_ERROR_MESSAGE);
}
while (true) {
const { started_at } = await get_status().catch(() => ({
started_at: first_timestamp,
}));
if (started_at !== first_timestamp) {
return;
}
await sleep(100);
}
}
async function wait_for_app_to_be_stable(n = 3) {
console.log("Waiting for app to be stable....");
let counter = 0;
while (true) {
const { status } = await get_status().catch(() => ({
status: "down",
}));
if (status == "running") {
console.log(counter);
counter++;
} else {
counter = 0;
}
if (counter == n) {
return;
}
await sleep(100);
}
}
async function wait_for_app_restart() {
try {
await wait_for_run_id_to_change();
} catch (e) {
if (e.message !== APP_DOWN_ERROR_MESSAGE) {
throw e;
}
}
await wait_for_app_to_be_stable();
}
export default class RefreshOnTSChanges extends Controller {
socket: WebSocket;
async connect() {
const { port, watch } = await fetch("/dist/notifier.json").then((r) => r.json());
if (!watch) {
console.warn(
"Not running auto refresh on watch because the build process is not running in watch mode"
);
return;
}
const socket = new WebSocket(`ws://localhost:${port}`);
socket.onmessage = async (message) => {
if (message.data.endsWith("-ts")) {
document.documentElement.classList.add("restarting");
await wait_for_app_restart();
document.documentElement.dispatchEvent(new Event("ts-rebuilt"));
document.documentElement.classList.remove("restarting");
}
};
}
async disconnect() {
this.socket.close();
}
}

View File

@ -0,0 +1,46 @@
import { Controller } from "stimulus";
function make_new_link() {
const new_link = document.createElement("link");
new_link.rel = "stylesheet";
new_link.href = `/dist/main.css?${Math.random()}+${Math.random()}`;
new_link.type = "text/css";
return new_link;
}
function getStyles() {
return Array.from(document.querySelectorAll("head link")).filter(
(e: HTMLLinkElement) => new URL(e.href).pathname == "/dist/main.css"
);
}
function cleanup_css() {
console.log("clearing styles");
getStyles()
.slice(0, -1)
.forEach((style) => {
style.parentElement.removeChild(style);
});
}
export default class RefreshStyles extends Controller {
socket: WebSocket;
async connect() {
const { port } = await fetch("/dist/notifier.json").then((r) => r.json());
this.socket = new WebSocket(`ws://localhost:${port}`);
this.socket.onmessage = async (message) => {
if (message.data === "css") {
const new_link = make_new_link();
new_link.onload = cleanup_css;
document.querySelector("head").appendChild(new_link);
}
};
document.documentElement.addEventListener("turbo:morph", cleanup_css);
}
async disconnect() {
this.socket.close();
document.documentElement.removeEventListener("turbo:morph", cleanup_css);
}
}

View File

@ -1,7 +1,6 @@
.map-with-pins { .map-with-pins {
#map {
height: 534px; height: 534px;
}
.popup { .popup {
.leaflet-popup-content-wrapper, .leaflet-popup-content-wrapper,
.leaflet-popup-tip { .leaflet-popup-tip {
@ -32,6 +31,7 @@
font-size: 12px; font-size: 12px;
line-height: 21px; line-height: 21px;
} }
.button { .button {
display: inline-block; display: inline-block;
margin: 0 auto; margin: 0 auto;

View File

@ -32,95 +32,36 @@ export class MapWithPins extends Component<typeof component_arguments> {
return component_arguments; return component_arguments;
} }
async getEarlyAssets() {
return [
{
type: "script" as const,
url: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
identity: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
integrity: "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=",
},
{
type: "style" as const,
url: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
integrity: "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=",
identity: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
},
];
}
toHTML({ toHTML({
pins, pins,
}: ExtractStructuredComponentArgumentsValues< }: ExtractStructuredComponentArgumentsValues<
typeof component_arguments typeof component_arguments
>): FlatTemplatable { >): FlatTemplatable {
return ( return (
<div class="map-with-pins"> <div
<link class="map-with-pins"
rel="stylesheet" data-controller="map-with-pins"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" data-map-with-pins-pins-value={JSON.stringify(pins)
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" .replaceAll("\n", "\\n")
crossorigin="" .replaceAll('"', "&quot;")}
/> ></div>
<script
onload="loadMap()"
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
{
/* HTML */ `<script>
function loadMap() {
const mapDiv = document.getElementById("map");
const resizeObserver = new ResizeObserver(() => {
map.invalidateSize();
});
resizeObserver.observe(mapDiv);
var map = L.map("map");
L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}
).addTo(map);
const pins = ${JSON.stringify(pins)};
pins.forEach((pin) => addPin(pin, map));
}
function addPin(pin, map) {
var pinIcon = L.icon({
iconUrl: "/pin-icon.svg",
iconSize: [29, 41],
iconAnchor: [14, 40],
popupAnchor: [-3, 14],
});
var marker = L.marker(
pin.coordinates.split(", ").map((x) => parseFloat(x)),
{
icon: pinIcon,
}
).addTo(map);
var popup = L.popup({
closeButton: false,
autoClose: false,
closeOnEscapeKey: false,
closeOnClick: false,
className: "popup",
offset: [0, -32],
maxWidth: "auto",
})
.setLatLng(
pin.coordinates.split(", ").map((x) => parseFloat(x))
)
.setContent(
/* HTML */ \`<div class="popup-content">
<p class="title">\${pin.title}</p>
<p class="address">\${pin.address}</p>
<a class="button" href="\${pin.button.link}">
\${pin.button.text}
</a>
</div> \`
)
.addTo(map);
map.setView(
pin.coordinates.split(", ").map((x) => parseFloat(x)),
13
);
}
</script>`
}
<div id="map"></div>
</div>
); );
} }
} }

View File

@ -0,0 +1,101 @@
import { Controller } from "stimulus";
declare const L: any;
export default class MapWithPins extends Controller {
id: string;
map: any;
initiated: boolean = false;
resizeObserver: ResizeObserver;
static values = {
pins: String,
};
connect() {
if (this.initiated) {
this.map.remove();
}
if (window.L) {
this.initiateMap();
} else {
document.addEventListener(
"loaded-https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
() => {
this.initiateMap();
}
);
}
}
disconnect() {
this.map.remove();
this.initiated = false;
this.resizeObserver.disconnect();
}
initiateMap() {
this.map = L.map(this.element);
this.resizeObserver = new ResizeObserver(() => {
this.map.invalidateSize();
});
this.resizeObserver.observe(this.element);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(this.map);
const pins = JSON.parse(
this.element.attributes["data-map-with-pins-pins-value"].value
);
pins.forEach((pin) => this.addPin(pin));
this.initiated = true;
}
pinsValueChanged() {
if (this.initiated) {
this.connect();
}
}
addPin(pin) {
var pinIcon = L.icon({
iconUrl: "/pin-icon.svg",
iconSize: [29, 41],
iconAnchor: [14, 40],
popupAnchor: [-3, 14],
});
var marker = L.marker(
pin.coordinates.split(", ").map((x) => parseFloat(x)),
{
icon: pinIcon,
}
).addTo(this.map);
var popup = L.popup({
closeButton: false,
autoClose: false,
closeOnEscapeKey: false,
closeOnClick: false,
className: "popup",
offset: [0, -32],
maxWidth: "auto",
})
.setLatLng(pin.coordinates.split(", ").map((x) => parseFloat(x)))
.setContent(
/* HTML */ `<div class="popup-content">
<p class="title">${pin.title}</p>
<p class="address">${pin.address}</p>
<a class="button" href="${pin.button.link}"> ${pin.button.text} </a>
</div> `
)
.addTo(this.map);
this.map.setView(
pin.coordinates.split(", ").map((x) => parseFloat(x)),
13
);
}
}

View File

@ -0,0 +1,3 @@
.nice-box {
background-color: white;
}

View File

@ -0,0 +1,60 @@
import { Controller } from "stimulus";
export default class ComponentDebugger extends Controller {
id: string;
main_form: HTMLFormElement;
is_resizing: boolean = false;
origin_x: number;
origin_width: number;
connect() {
this.main_form = document.querySelector("#component-debugger").closest("form");
document.documentElement.addEventListener("ts-rebuilt", () => {
this.main_form.requestSubmit();
});
this.main_form.addEventListener("turbo:submit-end", () => {
// this clears the values of file inputs, so they don't get unecessarily
// re-uploaded on future submissions - the file is alreade there on the server
this.main_form
.querySelectorAll("input[type=file]")
.forEach((input: HTMLInputElement) => (input.value = ""));
});
window.addEventListener("load", () => {
this.update_width_display();
});
document.addEventListener("turbo:render", () => this.update_width_display());
const gutter = this.targets.find("gutter") as HTMLDivElement;
gutter.addEventListener("mousedown", (e) => {
this.is_resizing = true;
this.origin_x = e.clientX;
const resizable = this.targets.find("preview") as HTMLSpanElement;
this.origin_width = resizable.getBoundingClientRect().width;
const handler = this.resizeHandler.bind(this);
document.addEventListener("mousemove", handler);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", handler);
});
e.preventDefault();
});
}
update_width_display() {
const component_width = (this.targets.find("preview") as HTMLSpanElement)
.offsetWidth;
(
this.targets.find("component-width") as HTMLSpanElement
).innerHTML = `(width: ${component_width}px)`;
}
resizeHandler(e: MouseEvent) {
const width_offset = this.origin_x - e.clientX;
const new_width = Math.max(this.origin_width + width_offset, 1);
document
.getElementById("component-debugger")
.style.setProperty("--resizable-column-width", new_width + "px");
this.update_width_display();
}
}

View File

@ -40,11 +40,7 @@ export async function ComponentInputTable<
return ( return (
<fieldset> <fieldset>
<legend>{arg_path.at(-1)}</legend> <legend>{arg_path.at(-1)}</legend>
<div <div>
style={`max-width: calc(var(--resizable-column-width) - ${
arg_path.length + 3
} * 14px); overflow-x: auto; max-height: 500px; overflow-y: auto;`}
>
<table style="position: relative; /* necessary for sticky th*/"> <table style="position: relative; /* necessary for sticky th*/">
<tbody> <tbody>
<tr> <tr>

View File

@ -5,7 +5,7 @@
.two-column { .two-column {
display: grid; display: grid;
grid-template-columns: min-content 15px 1fr; grid-template-columns: 1fr 15px min-content;
} }
.resize-gutter { .resize-gutter {
@ -15,7 +15,6 @@
} }
.resizable { .resizable {
width: var(--resizable-column-width);
overflow-x: auto; overflow-x: auto;
} }
@ -24,6 +23,20 @@
transform: scale(0.99); transform: scale(0.99);
opacity: 0.6; opacity: 0.6;
} }
.component-preview {
width: var(--resizable-column-width);
& > fieldset {
min-width: 0; /* default is min-content and that causes overflow*/
max-height: calc(100vh - 75px);
overflow-x: auto;
}
}
.component-arguments {
max-height: calc(100vh - 80px);
overflow-y: auto;
}
} }
.component-preview-parameters { .component-preview-parameters {

View File

@ -1,9 +1,9 @@
import { render } from "@sealcode/jdd"; import { render, renderEarlyAssets } from "@sealcode/jdd";
import { StateAndMetadata, StatefulPage, to_base64 } from "@sealcode/sealgen"; import { StatefulPage, to_base64 } from "@sealcode/sealgen";
import { hasFieldOfType, hasShape, predicates } from "@sealcode/ts-predicates"; import { hasShape, predicates } from "@sealcode/ts-predicates";
import { BaseContext } from "koa"; import { BaseContext } from "koa";
import { Templatable, TempstreamJSX } from "tempstream"; import { Templatable, tempstream, TempstreamJSX } from "tempstream";
import html from "../html.js"; import html, { defaultHead } from "../html.js";
import { registry } from "../jdd-components/components.js"; import { registry } from "../jdd-components/components.js";
import { ComponentInput } from "./component-preview/component-input.js"; import { ComponentInput } from "./component-preview/component-input.js";
import { ComponentPreviewActions } from "./component-preview/component-preview-actions.js"; import { ComponentPreviewActions } from "./component-preview/component-preview-actions.js";
@ -31,13 +31,33 @@ export default new (class ComponentsPage extends StatefulPage<
return initial_state; return initial_state;
} }
wrapInLayout(ctx: BaseContext, content: Templatable): Templatable { wrapInLayout(
return html(ctx, "Components", content, { ctx: BaseContext,
morphing: false, content: Templatable,
state: ComponentPreviewState
): Templatable {
return html(
ctx,
"Components",
content,
{
morphing: true,
preserveScroll: true, preserveScroll: true,
autoRefreshCSS: true, autoRefreshCSS: true,
navbar: () => ``, navbar: () => ``,
}); },
(...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry,
[
{
component_name: state.component,
args: state.component_args,
},
],
jdd_context
)}`
);
} }
wrapInForm(state: ComponentPreviewState, content: Templatable): Templatable { wrapInForm(state: ComponentPreviewState, content: Templatable): Templatable {
@ -96,8 +116,9 @@ export default new (class ComponentsPage extends StatefulPage<
class="two-column" class="two-column"
id="component-debugger" id="component-debugger"
style="--resizable-column-width: 50vw" style="--resizable-column-width: 50vw"
data-controller="component-debugger"
> >
<div class="resizable"> <div class="component-arguments">
{/*The below button has to be here in order for it to be the default behavior */} {/*The below button has to be here in order for it to be the default behavior */}
<input type="submit" value="Preview" /> <input type="submit" value="Preview" />
<select <select
@ -135,11 +156,12 @@ export default new (class ComponentsPage extends StatefulPage<
</fieldset> </fieldset>
<code>{JSON.stringify(state)}</code> <code>{JSON.stringify(state)}</code>
</div> </div>
<div class="resize-gutter"></div> <div class="resize-gutter" data-component-debugger-target="gutter"></div>
<div class="component-preview"> <div class="component-preview" data-component-debugger-target="preview">
<fieldset> <fieldset>
<legend> <legend>
Preview <span id="component_width_span"></span> Preview{" "}
<span data-component-debugger-target="component-width"></span>
</legend> </legend>
{render( {render(
registry, registry,
@ -155,75 +177,11 @@ export default new (class ComponentsPage extends StatefulPage<
{ {
/* HTML */ `<script> /* HTML */ `<script>
(function () { (function () {
function update_width_display() {
const component_width =
document.getElementsByClassName(
"component-preview"
)[0].offsetWidth;
document.getElementById(
"component_width_span"
).innerHTML = \`(width: \${component_width}px)\`;
}
window.addEventListener("load", (event) => {
update_width_display();
});
document.addEventListener(
"turbo:render",
update_width_display
);
let is_resizing = false;
let origin_x;
let origin_width;
const gutter = document.querySelector(".resize-gutter"); 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
.getElementById("component-debugger")
.style.setProperty(
"--resizable-column-width",
new_width + "px"
);
update_width_display();
};
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>` </script>`
} }
</div> </div>
{
/* HTML */ `<script>
const main_form = document
.querySelector("#component-debugger")
.closest("form");
document.documentElement.addEventListener("ts-rebuilt", () => {
main_form.requestSubmit();
});
main_form.addEventListener("turbo:submit-end", () => {
// this clears the values of file inputs, so they don't get unecessarily
// re-uploaded on future submissions - the file is alreade there on the server
main_form
.querySelectorAll("input[type=file]")
.forEach((input) => (input.value = ""));
});
</script>`
}
</div> </div>
); );
} }

22
src/front/controllers.ts Normal file
View File

@ -0,0 +1,22 @@
// DO NOT EDIT! This file is generated automaticaly with 'npm run generate-stimulus'
import * as Turbo from "@hotwired/turbo";
import { Application } from "stimulus";
const application = Application.start();
import { default as RefreshOnTsChanges } from "./../back/html/refresh-on-ts-changes.stimulus.js";
application.register("refresh-on-ts-changes", RefreshOnTsChanges);
import { default as RefreshStyles } from "./../back/html/refresh-styles.stimulus.js";
application.register("refresh-styles", RefreshStyles);
import { default as MapWithPins } from "./../back/jdd-components/map-with-pins/map-with-pins.stimulus.js";
application.register("map-with-pins", MapWithPins);
import { default as ComponentDebugger } from "./../back/routes/component-preview/component-debugger.stimulus.js";
application.register("component-debugger", ComponentDebugger);
import { default as InputImagePreview } from "./../back/routes/component-preview/input-image-preview.stimulus.js";
application.register("input-image-preview", InputImagePreview);
export { Turbo };

View File

@ -1,10 +1 @@
import * as Turbo from "@hotwired/turbo"; export * from "./controllers.js";
import { Application } from "stimulus";
import InputImagePreview from "./controllers/input-image-preview";
import TaskController from "./controllers/task-controller";
export { Turbo };
const application = Application.start();
application.register("task", TaskController);
application.register("input-image-preview", InputImagePreview);

View File

@ -3,6 +3,7 @@
@import "../node_modules/@sealcode/sealgen/src/forms/forms.css"; @import "../node_modules/@sealcode/sealgen/src/forms/forms.css";
@import "back/jdd-components/image-demo/image-demo.css"; @import "back/jdd-components/image-demo/image-demo.css";
@import "back/jdd-components/map-with-pins/map-with-pins.css"; @import "back/jdd-components/map-with-pins/map-with-pins.css";
@import "back/jdd-components/nice-box/nice-box.css";
@import "back/jdd-components/table/table.css"; @import "back/jdd-components/table/table.css";
@import "back/routes/common/ui/input.css"; @import "back/routes/common/ui/input.css";
@import "back/routes/components.css"; @import "back/routes/components.css";

View File

@ -8,7 +8,7 @@
"target": "ES2019", "target": "ES2019",
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["es2021"], "lib": ["es2021"],
"outDir": "../../dist/back", "outDir": "./dist/back",
"keyofStringsOnly": true, "keyofStringsOnly": true,
"jsx": "react", "jsx": "react",
"reactNamespace": "TempstreamJSX", "reactNamespace": "TempstreamJSX",
@ -19,6 +19,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"types": ["vitest/globals"] "types": ["vitest/globals"]
}, },
"include": ["./**/*", "./*"], "include": ["./src/back/*", "./src/back/**/*"],
"exclude": ["./src/front", "./src/**/*.stimulus.ts"],
"ts-node": { "experimentalResolver": true, "esm": true } "ts-node": { "experimentalResolver": true, "esm": true }
} }

View File

@ -5,5 +5,5 @@
"lib": ["dom"], "lib": ["dom"],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["./**/*", "./index.ts"] "include": ["./src/front", "./src/**/*.stimulus.ts"]
} }

6
tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"references": [
{ "path": "./tsconfig-front.json" },
{ "path": "./tsconfig-back.json" }
]
}