Merge remote-tracking branch 'playground/master'

This commit is contained in:
Kuba Orlik 2026-05-03 19:01:55 +02:00
commit cdf0c45e8d
18 changed files with 412 additions and 74 deletions

94
package-lock.json generated
View File

@ -14,9 +14,10 @@
"@koa/router": "^12.0.1",
"@sealcode/add-to-head": "^1.0.0",
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.8.4",
"@sealcode/jdd-editor": "^0.2.9",
"@sealcode/sealgen": "^0.19.6",
"@sealcode/jdd": "^0.8.6",
"@sealcode/jdd-editor": "^0.2.18",
"@sealcode/monaco-wrapper": "^0.0.4",
"@sealcode/sealgen": "^0.19.20",
"@sealcode/show-first-row": "^0.1.0",
"@sealcode/simplemde": "^1.12.1",
"@sealcode/sortable": "^0.0.1",
@ -25,6 +26,7 @@
"@types/leaflet": "^1.9.8",
"@types/object-hash": "^3.0.6",
"@types/simplemde": "^1.11.11",
"a11y.css": "^5.3.0",
"c8": "^10.1.3",
"colord": "^2.9.3",
"dotenv": "^16.4.5",
@ -32,6 +34,7 @@
"get-port": "^7.0.0",
"glightbox": "^3.3.1",
"glob": "^11.0.3",
"highlight.js": "^11.11.1",
"js-convert-case": "^4.2.0",
"koa-mount": "^4.2.0",
"koa-qs": "^3.0.0",
@ -924,8 +927,9 @@
}
},
"node_modules/@sealcode/file-manager": {
"version": "1.0.2",
"license": "ISC",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@sealcode/file-manager/-/file-manager-1.0.3.tgz",
"integrity": "sha512-S2E1Yh7an8i9LEnX1FNvGXZwiHeWmg4dwXhMZR62xaRP7JjUve/2630aIYdiL7Ci6WLOsI7XsPhEQDlbQvQTDA==",
"dependencies": {
"@types/mime-types": "^2.1.4",
"@types/uuid": "^9.0.8",
@ -935,8 +939,9 @@
}
},
"node_modules/@sealcode/jdd": {
"version": "0.8.4",
"license": "ISC",
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/@sealcode/jdd/-/jdd-0.8.7.tgz",
"integrity": "sha512-5kFackogIfsulfQlMezex1QZ/If2aT17C+RAtSArJbQIHi1oDqy9l8+yV4PPdgDNxuRK+M/AYI7hgexZpEpSKg==",
"dependencies": {
"@sealcode/file-manager": "^1.0.2",
"@sealcode/ts-predicates": "^0.5.3",
@ -957,12 +962,12 @@
}
},
"node_modules/@sealcode/jdd-editor": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@sealcode/jdd-editor/-/jdd-editor-0.2.9.tgz",
"integrity": "sha512-5/95i5ZiENX46irBHkiug3brNlrXYhpr+1nAX1OtqWKFQahgeERUExJUfjiTerwWs2N5HzpL/5VuElCR5wx49Q==",
"version": "0.2.18",
"resolved": "https://registry.npmjs.org/@sealcode/jdd-editor/-/jdd-editor-0.2.18.tgz",
"integrity": "sha512-14oT7FDjKWuH5aMm5C2jsdpFAGjz+cQjomj4aLMrMkPUtPSYtKV7CUxseM+OJKynSJl05iPTAtc1FebydqlRHQ==",
"dependencies": {
"@koa/router": "^13.1.0",
"@sealcode/jdd": "^0.8.3",
"@sealcode/jdd": "^0.8.6",
"@sealcode/monaco-wrapper": "^0.0.3",
"@sealcode/sealcodemirror": "^5.70.0-beta5",
"@sealcode/sealgen": "^0.19.6",
@ -991,13 +996,22 @@
"node": ">= 18"
}
},
"node_modules/@sealcode/jdd-editor/node_modules/highlight.js": {
"version": "11.11.1",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
"node_modules/@sealcode/jdd-editor/node_modules/@sealcode/monaco-wrapper": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@sealcode/monaco-wrapper/-/monaco-wrapper-0.0.3.tgz",
"integrity": "sha512-EAmcW5lDVeEMpdVfhxJM9psuBFgrD1Wr2JJrBg3ifkut4y5mTHuaHfkut3BrzgifuPeukUJZuyX+sncFs26c7g==",
"dependencies": {
"@sealcode/add-to-head": "^1.0.0",
"monaco-editor": "^0.52.2",
"stimulus": "^3.2.2",
"throttle-debounce": "^5.0.2"
}
},
"node_modules/@sealcode/jdd-editor/node_modules/monaco-editor": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ=="
},
"node_modules/@sealcode/jdd/node_modules/@sealcode/ts-predicates": {
"version": "0.5.3",
"license": "ISC"
@ -1016,8 +1030,9 @@
}
},
"node_modules/@sealcode/monaco-wrapper": {
"version": "0.0.3",
"license": "ISC",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@sealcode/monaco-wrapper/-/monaco-wrapper-0.0.4.tgz",
"integrity": "sha512-oRksJVfCLPVabExFF4hbzUBS/LVCLRfdqsJp4VbTcd0NPa6kmRs/hDxD+XL1kZ0xkt3EeFeH5gHRmRMtPDAmtA==",
"dependencies": {
"@sealcode/add-to-head": "^1.0.0",
"monaco-editor": "^0.52.2",
@ -1034,8 +1049,9 @@
"license": "MIT"
},
"node_modules/@sealcode/sealgen": {
"version": "0.19.6",
"license": "ISC",
"version": "0.19.20",
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.19.20.tgz",
"integrity": "sha512-x7sI3/C7WtaM23v2lF540Dn0FNw1cAvBfi14Mw4cNHjZKQS7JOv2PjODwm+DOMCTsFtUrz/ElNBCvos0H34tjw==",
"dependencies": {
"@koa/router": "^12.0.1",
"@sealcode/file-manager": "^1.0.2",
@ -1071,7 +1087,7 @@
"peerDependencies": {
"koa": "^2.13.0",
"koa-responsive-image-router": "^0.2.24",
"sealious": "^0.21.20",
"sealious": "^0.21.36",
"stimulus": "^3.2.2"
}
},
@ -1936,6 +1952,11 @@
"license": "Apache-2.0",
"peer": true
},
"node_modules/a11y.css": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/a11y.css/-/a11y.css-5.3.0.tgz",
"integrity": "sha512-WBEnwAT3NHyJFN0ejedhc4Q0U4yX2fNWmoee8Ai6TWIICjrocTNugXm8XFuIC8dI8goaIoqxnSd4PlnljJ8dxw=="
},
"node_modules/abbrev": {
"version": "2.0.0",
"license": "ISC",
@ -2995,7 +3016,8 @@
},
"node_modules/color-convert": {
"version": "3.1.3",
"license": "MIT",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
"dependencies": {
"color-name": "^2.0.0"
},
@ -3009,14 +3031,16 @@
},
"node_modules/color-name": {
"version": "2.1.0",
"license": "MIT",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
"engines": {
"node": ">=12.20"
}
},
"node_modules/color-string": {
"version": "2.1.4",
"license": "MIT",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
"dependencies": {
"color-name": "^2.0.0"
},
@ -5909,10 +5933,11 @@
}
},
"node_modules/highlight.js": {
"version": "10.7.3",
"license": "BSD-3-Clause",
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"engines": {
"node": "*"
"node": ">=12.0.0"
}
},
"node_modules/hookable": {
@ -10548,11 +10573,12 @@
"peer": true
},
"node_modules/sealious": {
"version": "0.21.30",
"license": "BSD-2-Clause",
"version": "0.21.37",
"resolved": "https://registry.npmjs.org/sealious/-/sealious-0.21.37.tgz",
"integrity": "sha512-sSIBtaOsHl9NUxm0jOnSs79QVh4mdongjd+tzOr8e8fqG8GPiSy8/RTMNwaUF7+csVW3OvEaa98Da7YD3LRfoA==",
"dependencies": {
"@koa/router": "^12.0.1",
"@sealcode/file-manager": "^1.0.1",
"@sealcode/file-manager": "^1.0.3",
"@sealcode/ts-predicates": "^0.4.3",
"@types/boom": "^7.3.0",
"@types/clone": "^0.1.30",
@ -12481,6 +12507,14 @@
"node": ">=6 <7 || >=8"
}
},
"node_modules/typedoc/node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
"engines": {
"node": "*"
}
},
"node_modules/typedoc/node_modules/jsonfile": {
"version": "4.0.0",
"license": "MIT",

View File

@ -86,6 +86,10 @@
{
"from": "node_modules/glightbox/dist",
"to": "dist/glightbox"
},
{
"from": "node_modules/a11y.css/css",
"to": "dist/a11y.css"
}
]
},
@ -97,9 +101,10 @@
"@koa/router": "^12.0.1",
"@sealcode/add-to-head": "^1.0.0",
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.8.4",
"@sealcode/jdd-editor": "^0.2.9",
"@sealcode/sealgen": "^0.19.6",
"@sealcode/jdd": "^0.8.6",
"@sealcode/jdd-editor": "^0.2.18",
"@sealcode/monaco-wrapper": "^0.0.4",
"@sealcode/sealgen": "^0.19.20",
"@sealcode/show-first-row": "^0.1.0",
"@sealcode/simplemde": "^1.12.1",
"@sealcode/sortable": "^0.0.1",
@ -108,6 +113,7 @@
"@types/leaflet": "^1.9.8",
"@types/object-hash": "^3.0.6",
"@types/simplemde": "^1.11.11",
"a11y.css": "^5.3.0",
"c8": "^10.1.3",
"colord": "^2.9.3",
"dotenv": "^16.4.5",
@ -115,6 +121,7 @@
"get-port": "^7.0.0",
"glightbox": "^3.3.1",
"glob": "^11.0.3",
"highlight.js": "^11.11.1",
"js-convert-case": "^4.2.0",
"koa-mount": "^4.2.0",
"koa-qs": "^3.0.0",

View File

@ -0,0 +1,11 @@
/* we properly embed charset metadata, but Turbo changes the order of elements in <head> and this rule gives a false positive */
head :first-child:not([charset]) {
&,
&::after {
display: none !important;
}
}
head :first-child:not([charset]) ~ link:last-of-type::before {
display: none !important;
}

View File

@ -40,7 +40,12 @@ export const IMAGE_CACHE_FS_DIR =
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/images");
export const SMARTCROP_CACHE_FS_DIR =
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/smartcrop");
export const IMAGE_CACHE_SIZE = parseInt(process.env.IMAGE_CACHE_SIZE || "1000");
export const IMAGE_SMARTCROP_CACHE_SIZE = parseInt(process.env.IMAGE_CACHE_SIZE || "500");
export const IMAGE_THUMBNAIL_CACHE_SIZE = parseInt(process.env.IMAGE_CACHE_SIZE || "500");
export const UPLOADS_FS_DIR =
process.env.UPLOADS_FS_DIR || locreq.resolve("uploaded_files");
export const MEILISEARCH_MASTER_KEY = process.env.MEILISEARCH_MASTER_KEY || "qwerty";
export const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST || "http://localhost:1098";
export const SCAN_A11Y = false;

View File

@ -3,6 +3,7 @@ import type { Readable } from "stream";
import type { Context } from "koa";
import type { HTMLOptions } from "@sealcode/sealgen";
import { htmlEscape } from "escape-goat";
import { SCAN_A11Y } from "./config.js";
export const start_timestamp = Date.now();
@ -24,13 +25,16 @@ export function defaultHead({
description: string;
}): JSX.Element | Readable {
const origin = ctx.URL.origin;
return tempstream /* HTML */ `<title>${title}</title>
return tempstream /* HTML */ `<meta charset="utf-8" /> <title>${title}</title>
${SCAN_A11Y ? `<link rel="stylesheet" href="/dist/a11y.css/a11y-en.css">` : ""}
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${htmlEscape(description)}" />
${ctx.$app.getFeedHTMLMetatags(ctx)}
<script defer src="/dist/bundle.js?v=${start_timestamp}"></script>
${metaImage ? `<meta property="og:image" content="${metaImage}" />` : ""}
${metaImage
? `<meta property="og:image" content="${metaImage.startsWith("/") ? origin + metaImage : metaImage}" />`
: ""}
${[
"default",
"page",

View File

@ -1,10 +1,10 @@
import type { HTMLArgs, HTMLOptions } from "@sealcode/sealgen";
import { tempstreamAsync } from "tempstream";
import type { Readable } from "stream";
import { toKebabCase } from "js-convert-case";
import type { Readable } from "stream";
import { tempstreamAsync } from "tempstream";
import { DEFAULT_HTML_LANG } from "./config.js";
import { default_navbar } from "./routes/common/navbar.js";
import { defaultHead } from "./defaultHead.js";
import { default_navbar } from "./routes/common/navbar.js";
const default_html_options: Partial<HTMLOptions> = {
showFooter: true,
@ -40,6 +40,8 @@ export default function html({
controllers.push("refresh-on-ts-changes");
}
const classes = (htmlOptions?.bodyClasses || []).join(" ");
return tempstreamAsync /* HTML */ ` <!DOCTYPE html>
<html
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
@ -58,7 +60,7 @@ export default function html({
</head>
<body
data-controller="${controllers.join(" ")}"
class="${(htmlOptions?.bodyClasses || []).join(" ")}"
${classes ? `class="${classes}"` : ""}
>
${!hideNavigation ? (htmlOptions?.navbar || default_navbar)(ctx) : ""}
${body}

View File

@ -1,14 +1,23 @@
import { KoaResponsiveImageRouter } from "koa-responsive-image-router";
import { IMAGE_CACHE_FS_DIR, SMARTCROP_CACHE_FS_DIR } from "./config.js";
import {
IMAGE_CACHE_FS_DIR,
SMARTCROP_CACHE_FS_DIR,
IMAGE_CACHE_SIZE,
IMAGE_SMARTCROP_CACHE_SIZE,
IMAGE_THUMBNAIL_CACHE_SIZE,
} from "./config.js";
export const RESPONSIVE_IMAGES_URL_PATH = "/images";
export const imageRouter = new KoaResponsiveImageRouter({
staticPath: RESPONSIVE_IMAGES_URL_PATH,
thumbnailSize: 20,
cacheManagerResolutionThreshold: 50,
cacheManagerResolutionThreshold: 21,
imageStoragePath: IMAGE_CACHE_FS_DIR,
smartCropStoragePath: SMARTCROP_CACHE_FS_DIR,
formatsForLossy: ["webp"],
formatsForLossless: ["webp"],
diskImageCacheSize: IMAGE_CACHE_SIZE,
smartCropCacheSize: IMAGE_SMARTCROP_CACHE_SIZE,
thumbnailMaxCacheSize: IMAGE_THUMBNAIL_CACHE_SIZE,
});

View File

@ -0,0 +1,22 @@
.code-snippet {
display: flex;
flex-direction: column;
padding: 0 16px 16px 16px;
}
.code-snippet pre {
margin: 0;
}
.code-snippet__language-error {
color: red;
font-size: 16px;
}
.code-snippet__language {
display: flex;
justify-content: flex-start;
margin: 0;
margin-top: 20px;
gap: 8px;
}

View File

@ -0,0 +1,54 @@
import { TempstreamJSX } from "tempstream";
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
import hljs from "highlight.js";
const component_arguments = {
codeWithLanguage: new ComponentArguments.CodeWithCustomLanguage(),
} as const;
export class CodeSnippet extends Component<typeof component_arguments> {
getArguments() {
return component_arguments;
}
async toHTML({
args: { codeWithLanguage },
classes,
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
let highlightedCode: string;
let languageNotFoundMessage: string = "";
const language = codeWithLanguage.language?.toLowerCase() || "plaintext";
if (hljs.getLanguage(language)) {
highlightedCode = hljs.highlight(codeWithLanguage.code, {
language,
}).value;
} else {
highlightedCode = hljs.highlight(codeWithLanguage.code, {
language: "plaintext",
}).value;
languageNotFoundMessage = `Language "${language}" not found`;
}
return (
<div class={["code-snippet", ...classes]} data-controller="code-snippet">
<h2>CodeSnippet</h2>
{languageNotFoundMessage ? (
<p class="code-snippet__language-error">{languageNotFoundMessage}</p>
) : (
<>
<pre>
<code data-code-snippet-target="code">{highlightedCode}</code>
</pre>
<div class="code-snippet__language">
<span>{codeWithLanguage.language}</span>
<button data-action="code-snippet#copyCode">Copy</button>
</div>
</>
)}
</div>
);
}
}

View File

@ -55,7 +55,7 @@ export class FaqComponent extends Component<typeof component_arguments> {
>
<summary class="question">{element.question}</summary>
<div class="answer">
<p>{render_markdown(language, element.answer)}</p>
{render_markdown(language, element.answer)}
</div>
</details>
))}

View File

@ -0,0 +1,58 @@
.breadcrumbs {
margin-top: 16px;
margin-left: 16px;
--bg: var(--color-brand-accent);
display: flex;
--height: 2rem;
--padding: 0px 16px;
flex-flow: row wrap;
align-items: center;
row-gap: 8px;
span {
padding: var(--padding);
}
a,
& > span {
display: inline-block;
line-height: var(--height);
font-size: 1rem;
}
a {
text-decoration: none;
display: flex;
flex-flow: row nowrap;
color: var(--color-brand-text-on-accent);
span {
background-color: var(--bg);
}
.arrow-right,
.arrow-left {
width: 0;
height: 0;
}
.arrow-right {
border-top: calc(var(--height) / 2) solid transparent;
border-bottom: calc(var(--height) / 2) solid transparent;
border-left: calc(var(--height) / 2) solid var(--bg);
}
.arrow-left {
width: 0;
height: 0;
border-top: calc(var(--height) / 2) solid var(--bg);
border-bottom: calc(var(--height) / 2) solid var(--bg);
border-left: calc(var(--height) / 2) solid transparent;
}
&:hover {
filter: brightness(1.1);
}
}
}

View File

@ -0,0 +1,71 @@
import type { Context } from "koa";
import { TempstreamJSX } from "tempstream";
import { get_breadcrumbs_from_path } from "../url-tree.js";
const arrow_width = 8;
const arrow_height = 28;
function arrow_tail() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={`${arrow_width}px`}
height={`${arrow_height}px`}
viewBox="0 0 ${arrow_width} ${arrow_height}"
style="margin-right: -1px"
>
{
/* HTML */ `<path
d="M 0 0 ${arrow_width} 0l0 ${arrow_height} -${arrow_width} 0L${arrow_width} ${arrow_height /
2}Z"
style="fill:var(--bg);"
/>`
}
</svg>
);
}
function arrow_head() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={`${arrow_width}px`}
height={`${arrow_height}px`}
viewBox="0 0 ${arrow_width} ${arrow_height}"
>
{
/* HTML */ `<path
d="m 0 0 ${arrow_width} ${arrow_height / 2}L0 ${arrow_height}Z"
style="fill:var(--bg);"
/>`
}
</svg>
);
}
export async function breadcrumbs(ctx: Context) {
const breadcrumbs = await Promise.all(
get_breadcrumbs_from_path(ctx.path).map(async (e, i, all) => {
if (!e.actionName) {
return "";
}
const breadcrumbLabel = e.breadcrumbLabel?.({ ...ctx });
const breadcrumbLabelResult = await breadcrumbLabel;
if (i == all.length - 1) {
return <span>{breadcrumbLabelResult || e.actionName}</span>;
}
return (
<>
<a href={e.url}>
{i !== 0 ? arrow_tail() : ""}
<span>{breadcrumbLabelResult || e.actionName}</span>
{arrow_head()}
</a>
</>
);
})
);
return <div class="breadcrumbs">{breadcrumbs.filter((e) => e !== "").join("")}</div>;
}

View File

@ -1,9 +1,10 @@
import type { BaseContext } from "koa";
import type { Context } from "koa";
import { button } from "src/back/elements/button.js";
import { tempstream } from "tempstream";
import type { FlatTemplatable } from "tempstream";
import { breadcrumbs } from "./breadcrumbs.js";
export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable> {
export async function default_navbar(ctx: Context): Promise<FlatTemplatable> {
const { items: navbar_items } = await ctx.$app.collections["navbar-links"]
.list(ctx.$context)
.fetch();
@ -25,18 +26,20 @@ export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable>
)
.join("\n");
return /* HTML */ `<nav>
<a href="/" class="nav-logo">
<img
src="/assets/logo"
alt="${ctx.$app.manifest.name} - logo"
width="867"
height="230"
style="width:220px; height: auto"
/>
</a>
<ul>
${linksHTML}
</ul>
</nav>`;
return tempstream /* HTML */ `<nav>
<a href="/" class="nav-logo">
<img
src="/assets/logo"
alt="${ctx.$app.manifest.name} - logo"
width="867"
height="230"
style="width:220px; height: auto"
/>
</a>
<ul>
${linksHTML}
</ul>
</nav>
${breadcrumbs(ctx)} `;
}

View File

@ -2,9 +2,12 @@ import type { Context } from "koa";
import { TempstreamJSX } from "tempstream";
import { Page } from "@sealcode/sealgen";
import type { BreadcrumbLabel } from "@sealcode/sealgen";
import html from "src/back/html.js";
export const actionName = "HelloPage";
export const breadcrumbLabel: BreadcrumbLabel = "Witaj";
export default new (class HelloPagePage extends Page {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -67,7 +67,7 @@ export default new (class LoginForm extends Form<typeof fields, void> {
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
return html({
ctx,
title: "Form",
title: "Sign in",
description: "",
css_clumps: ["admin-forms"],
body: super.render(ctx, data, show_field_errors) as Promise<string>,

View File

@ -0,0 +1,49 @@
import * as path from "path";
import type { Context } from "koa";
import { url_tree } from "./routes.js";
import type { URLTree } from "./routes.js";
import type { BreadcrumbLabel } from "@sealcode/sealgen";
export function get_breadcrumbs_from_path(url: string) {
let position: URLTree = url_tree;
const elements = url.split("/").filter((e) => e != "");
const breadcrumbs: {
actionName?: string;
url?: string;
breadcrumbLabel?: (ctx: Context) => Promise<string>;
}[] = [{ actionName: "Home", url: "/" }];
let path_so_far = "";
for (const element of elements) {
if (position.children[element]) {
position = position.children[element];
} else if (Object.keys(position.children).find((e) => e.startsWith(":"))) {
const key = Object.keys(position.children).find((e) => e.startsWith(":"));
position = position.children[key!]!;
} else {
break;
}
path_so_far += "/" + element;
const actionName = position.actionName;
const modulePath = position.module_path;
breadcrumbs.push({
actionName: actionName,
url: path_so_far,
breadcrumbLabel: async (ctx: Context) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/consistent-type-assertions
const module = (await import(path.resolve(modulePath || "") || "")) as {
breadcrumbLabel: BreadcrumbLabel;
};
if (!module.breadcrumbLabel) return actionName || "";
if (typeof module.breadcrumbLabel === "function") {
return module.breadcrumbLabel(ctx);
} else {
return module.breadcrumbLabel;
}
},
});
}
return breadcrumbs;
}

View File

@ -22,6 +22,9 @@ application.register("glightbox", Glightbox);
import { default as AutogrowTextarea } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/autogrow-textarea.stimulus.js";
application.register("autogrow-textarea", AutogrowTextarea);
import { default as CodeSnippet } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/code-snippet.stimulus.js";
application.register("code-snippet", CodeSnippet);
import { default as ComponentDebugger } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/component-debugger.stimulus.js";
application.register("component-debugger", ComponentDebugger);
@ -37,9 +40,6 @@ application.register("jdd-table-paste", JddTablePaste);
import { default as JsonEditor } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/json-editor.stimulus.js";
application.register("json-editor", JsonEditor);
import { default as MarkdownTextarea } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/markdown-textarea.stimulus.js";
application.register("markdown-textarea", MarkdownTextarea);
import { default as RefreshOnTsChanges } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/refresh-on-ts-changes.stimulus.js";
application.register("refresh-on-ts-changes", RefreshOnTsChanges);
@ -55,8 +55,11 @@ application.register("toast", Toast);
import { default as Monaco } from "./../../node_modules/@sealcode/monaco-wrapper/src/controllers/monaco.stimulus.js";
application.register("monaco", Monaco);
import { default as TableAddButton } from "./../../node_modules/@sealcode/sealgen/src/controllers/table-add-button.stimulus.js";
application.register("table-add-button", TableAddButton);
import { default as PasswordGenerateButton } from "./../../node_modules/@sealcode/sealgen/src/controllers/password-generate-button.stimulus.js";
application.register("password-generate-button", PasswordGenerateButton);
import { default as SealgenTable } from "./../../node_modules/@sealcode/sealgen/src/controllers/sealgen-table.stimulus.js";
application.register("sealgen-table", SealgenTable);
import { default as Sortable } from "./../../node_modules/@sealcode/sortable/src/controllers/sortable.stimulus.js";
application.register("sortable", Sortable);

View File

@ -1,3 +1,7 @@
* {
box-sizing: border-box;
}
html {
background: var(--color-brand-canvas);
font-family: var(--font-sans-serif);
@ -103,16 +107,11 @@ h4 {
display: grid !important;
--min-content-width: 320px;
--main-column-max-width: 1224px;
grid-template-columns: [screen-left-edge] minmax(
16px,
1fr
) [content-left-edge left-column-start] minmax(
120px,
260px
) [left-column-end] 24px [right-column-start] minmax(
120px,
260px
) [content-right-edge right-column-end] minmax(16px, 1fr) [screen-right-edge];
grid-template-columns:
[screen-left-edge] minmax(16px, 1fr)
[content-left-edge left-column-start] minmax(120px, 260px)
[left-column-end] 24px [right-column-start] minmax(120px, 260px)
[content-right-edge right-column-end] minmax(16px, 1fr) [screen-right-edge];
@container (max-width: 550px) {
grid-template-columns:
@ -153,3 +152,7 @@ h4 {
}
}
}
.sealious-list-wrapper {
padding: 16px;
}