Add rudimentary support for automatic breadcrumbs — currently only with ActionName as labels

This commit is contained in:
Kuba Orlik 2026-01-02 21:02:48 +01:00
parent b80ce89fe8
commit 47f0214fe3
7 changed files with 195 additions and 42 deletions

44
package-lock.json generated
View File

@ -16,7 +16,7 @@
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.8.3",
"@sealcode/jdd-editor": "^0.2.9",
"@sealcode/sealgen": "^0.19.6",
"@sealcode/sealgen": "^0.19.17",
"@sealcode/show-first-row": "^0.1.0",
"@sealcode/simplemde": "^1.12.1",
"@sealcode/sortable": "^0.0.1",
@ -1342,9 +1342,9 @@
}
},
"node_modules/@sealcode/file-manager": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@sealcode/file-manager/-/file-manager-1.0.2.tgz",
"integrity": "sha512-BOMgC90QffE9cVFKkLVTjDbUJ5WB9YqcmS4fwqFxgnnC3YlH9xb9rff3iGXSkKOHm0kCeSjq0Ohasxtq/z72WQ==",
"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",
@ -1462,9 +1462,9 @@
"integrity": "sha512-pDsGlk2KokQkwzsJDBUWJFDRpEoxxth6TMQGDJyCTmWnd1Vn+cQb5moXDKaf7cXnWb9Y6QtdNX/fPzM/3RH2Cg=="
},
"node_modules/@sealcode/sealgen": {
"version": "0.19.6",
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.19.6.tgz",
"integrity": "sha512-dUccXEGSTlZrkrzkta2cObQUDBzbTrfYLZkbZer6E9nyYKB/7Jbj03R+C3wGxfwvrsewGfHiar+v8JHjOiguyQ==",
"version": "0.19.17",
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.19.17.tgz",
"integrity": "sha512-QDhtLhgEwRDgaLx2efTZZII1jq5PGLVUlGOF6S4vIcSl4jA9UGrljPbw0JE5Opk8LY1oZE/Xkjz5rz8tsSFe3A==",
"dependencies": {
"@koa/router": "^12.0.1",
"@sealcode/file-manager": "^1.0.2",
@ -1500,7 +1500,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"
}
},
@ -12093,12 +12093,12 @@
"peer": true
},
"node_modules/sealious": {
"version": "0.21.30",
"resolved": "https://registry.npmjs.org/sealious/-/sealious-0.21.30.tgz",
"integrity": "sha512-EWmqqFpDdgMvBmqAnaoX3FN2TmWdEgA1MgCUoQNB0iwUSl5m2Ghpd3W3eoM4H0Nr6bTr1ogJaOsAZJJFlIdc0Q==",
"version": "0.21.36",
"resolved": "https://registry.npmjs.org/sealious/-/sealious-0.21.36.tgz",
"integrity": "sha512-K1Gm7nM/fg8AEnd9QAJBSpoF6qJSuuwNlWjpCvz69sJkQ9FR8N3uRoX5t+gOPu7xqmBsrlNOx7JyF3sg3WQsFQ==",
"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",
@ -15954,9 +15954,9 @@
}
},
"@sealcode/file-manager": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@sealcode/file-manager/-/file-manager-1.0.2.tgz",
"integrity": "sha512-BOMgC90QffE9cVFKkLVTjDbUJ5WB9YqcmS4fwqFxgnnC3YlH9xb9rff3iGXSkKOHm0kCeSjq0Ohasxtq/z72WQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@sealcode/file-manager/-/file-manager-1.0.3.tgz",
"integrity": "sha512-S2E1Yh7an8i9LEnX1FNvGXZwiHeWmg4dwXhMZR62xaRP7JjUve/2630aIYdiL7Ci6WLOsI7XsPhEQDlbQvQTDA==",
"requires": {
"@types/mime-types": "^2.1.4",
"@types/uuid": "^9.0.8",
@ -16061,9 +16061,9 @@
"integrity": "sha512-pDsGlk2KokQkwzsJDBUWJFDRpEoxxth6TMQGDJyCTmWnd1Vn+cQb5moXDKaf7cXnWb9Y6QtdNX/fPzM/3RH2Cg=="
},
"@sealcode/sealgen": {
"version": "0.19.6",
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.19.6.tgz",
"integrity": "sha512-dUccXEGSTlZrkrzkta2cObQUDBzbTrfYLZkbZer6E9nyYKB/7Jbj03R+C3wGxfwvrsewGfHiar+v8JHjOiguyQ==",
"version": "0.19.17",
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.19.17.tgz",
"integrity": "sha512-QDhtLhgEwRDgaLx2efTZZII1jq5PGLVUlGOF6S4vIcSl4jA9UGrljPbw0JE5Opk8LY1oZE/Xkjz5rz8tsSFe3A==",
"requires": {
"@koa/router": "^12.0.1",
"@sealcode/file-manager": "^1.0.2",
@ -24023,12 +24023,12 @@
}
},
"sealious": {
"version": "0.21.30",
"resolved": "https://registry.npmjs.org/sealious/-/sealious-0.21.30.tgz",
"integrity": "sha512-EWmqqFpDdgMvBmqAnaoX3FN2TmWdEgA1MgCUoQNB0iwUSl5m2Ghpd3W3eoM4H0Nr6bTr1ogJaOsAZJJFlIdc0Q==",
"version": "0.21.36",
"resolved": "https://registry.npmjs.org/sealious/-/sealious-0.21.36.tgz",
"integrity": "sha512-K1Gm7nM/fg8AEnd9QAJBSpoF6qJSuuwNlWjpCvz69sJkQ9FR8N3uRoX5t+gOPu7xqmBsrlNOx7JyF3sg3WQsFQ==",
"requires": {
"@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",

View File

@ -95,7 +95,7 @@
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.8.3",
"@sealcode/jdd-editor": "^0.2.9",
"@sealcode/sealgen": "^0.19.6",
"@sealcode/sealgen": "^0.19.17",
"@sealcode/show-first-row": "^0.1.0",
"@sealcode/simplemde": "^1.12.1",
"@sealcode/sortable": "^0.0.1",

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,

View File

@ -0,0 +1,56 @@
.breadcrumbs {
--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 function breadcrumbs(ctx: Context) {
return (
<div class="breadcrumbs">
{get_breadcrumbs_from_path(ctx.path)
.map((e, i, all) => {
if (!e.actionName) {
return "";
}
if (i == all.length - 1) {
return <span>{e.actionName}</span>;
}
return (
<>
<a href={e.url}>
{i !== 0 ? arrow_tail() : ""}
<span>{e.actionName}</span>
{arrow_head()}
</a>
</>
);
})
.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,19 @@ 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="50"
height="50"
/>
${ctx.$app.manifest.name}
</a>
<ul>
${linksHTML}
</ul>
</nav>`;
return /* HTML */ tempstream`<nav>
<a href="/" class="nav-logo">
<img
src="/assets/logo"
alt="${ctx.$app.manifest.name} - logo"
width="50"
height="50"
/>
${ctx.$app.manifest.name}
</a>
<ul>
${linksHTML}
</ul>
</nav>
${breadcrumbs(ctx)} `;
}

View File

@ -0,0 +1,24 @@
import { url_tree } from "./routes.js";
import type { URLTree } from "./routes.js";
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 }[] = [
{ 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;
breadcrumbs.push({ actionName: position.actionName, url: path_so_far });
}
return breadcrumbs;
}