From 47f0214fe3f6dc1be6b6b9c692c25373ed0f295d Mon Sep 17 00:00:00 2001 From: Kuba Orlik Date: Fri, 2 Jan 2026 21:02:48 +0100 Subject: [PATCH] =?UTF-8?q?Add=20rudimentary=20support=20for=20automatic?= =?UTF-8?q?=20breadcrumbs=20=E2=80=94=20currently=20only=20with=20ActionNa?= =?UTF-8?q?me=20as=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 44 ++++++++-------- package.json | 2 +- src/back/html.tsx | 6 +-- src/back/routes/common/breadcrumbs.css | 56 ++++++++++++++++++++ src/back/routes/common/breadcrumbs.tsx | 71 ++++++++++++++++++++++++++ src/back/routes/common/navbar.ts | 34 ++++++------ src/back/routes/url-tree.ts | 24 +++++++++ 7 files changed, 195 insertions(+), 42 deletions(-) create mode 100644 src/back/routes/common/breadcrumbs.css create mode 100644 src/back/routes/common/breadcrumbs.tsx create mode 100644 src/back/routes/url-tree.ts diff --git a/package-lock.json b/package-lock.json index 9a3d2a9..850eee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 83d4c25..7ef020a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/back/html.tsx b/src/back/html.tsx index ef025ca..438c15a 100644 --- a/src/back/html.tsx +++ b/src/back/html.tsx @@ -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 = { showFooter: true, diff --git a/src/back/routes/common/breadcrumbs.css b/src/back/routes/common/breadcrumbs.css new file mode 100644 index 0000000..f782103 --- /dev/null +++ b/src/back/routes/common/breadcrumbs.css @@ -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); + } + } +} diff --git a/src/back/routes/common/breadcrumbs.tsx b/src/back/routes/common/breadcrumbs.tsx new file mode 100644 index 0000000..a41e954 --- /dev/null +++ b/src/back/routes/common/breadcrumbs.tsx @@ -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 ( + + { + /* HTML */ `` + } + + ); +} + +function arrow_head() { + return ( + + { + /* HTML */ `` + } + + ); +} + +export function breadcrumbs(ctx: Context) { + return ( + + ); +} diff --git a/src/back/routes/common/navbar.ts b/src/back/routes/common/navbar.ts index 853ce3f..d10cc0f 100644 --- a/src/back/routes/common/navbar.ts +++ b/src/back/routes/common/navbar.ts @@ -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 { +export async function default_navbar(ctx: Context): Promise { 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 ) .join("\n"); - return /* HTML */ ``; + return /* HTML */ tempstream` + ${breadcrumbs(ctx)} `; } diff --git a/src/back/routes/url-tree.ts b/src/back/routes/url-tree.ts new file mode 100644 index 0000000..864bf82 --- /dev/null +++ b/src/back/routes/url-tree.ts @@ -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; +}