-
{t("compare.title")}
+
+ {t("compare.title")}
+
+
{t("compare.subtitle", { count: list.length })}
diff --git a/artifacts/toolrate/src/pages/docs.tsx b/artifacts/toolrate/src/pages/docs.tsx
index c16ec5c..4c622e2 100644
--- a/artifacts/toolrate/src/pages/docs.tsx
+++ b/artifacts/toolrate/src/pages/docs.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useState, createContext, useContext } from "react";
import { Link, useLocation } from "wouter";
import { Marked } from "marked";
import DOMPurify from "dompurify";
@@ -42,6 +42,21 @@ import {
const DOCS_BASE = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/docs`;
const REPO_URL = "https://git.kubebase.de/admin/tool-evaluator";
+// Active docs version (null = current docs). Shared via context so every
+// sub-view builds versioned links and file paths.
+const DocsVersionContext = createContext
(null);
+const useDocsVersion = () => useContext(DocsVersionContext);
+
+// Full in-app URL for a docs path, prefixed with the active version.
+function docsHref(version: string | null, path: string) {
+ return version === null ? `/docs/${path}` : `/docs/${version}/${path}`;
+}
+
+// Static file path under /docs, prefixed with the versioned snapshot dir.
+function docsFile(version: string | null, relPath: string) {
+ return version === null ? relPath : `versions/${version}/${relPath}`;
+}
+
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
@@ -88,6 +103,7 @@ type ReleaseDoc = {
title: string;
date: string | null;
hasReference: boolean;
+ hasHandbook: boolean;
};
type HandbookPage = { slug: string; file: string; title: string; order: number };
@@ -230,8 +246,11 @@ function isLinkableType(t: FieldType): boolean {
}
function resolveTypeHref(t: FieldType): string | null {
- if (t.kind === "ref") return `/docs/reference/schemas/${t.value}`;
- if (t.kind === "array" && /^[A-Z]/.test(t.value)) return `/docs/reference/schemas/${t.value}`;
+ const version = useDocsVersion();
+ const href = (v: string | null) =>
+ v === null ? `/docs/reference/schemas/${t.value}` : `/docs/${v}/reference/schemas/${t.value}`;
+ if (t.kind === "ref") return href(version);
+ if (t.kind === "array" && /^[A-Z]/.test(t.value)) return href(version);
return null;
}
@@ -328,14 +347,14 @@ function DocsNav({
const groups: { label: string; icon: LucideIcon; items: { href: string; label: string; active: boolean }[] }[] = [];
- if (handbook && handbook.length > 0 && version === null) {
+ if (handbook && handbook.length > 0) {
groups.push({
label: t("docs.guides"),
icon: BookOpen,
items: handbook.map((p) => ({
- href: `/docs/handbook/${p.slug}`,
+ href: docsHref(version, `handbook/${p.slug}`),
label: p.title,
- active: navLink(`/docs/handbook/${p.slug}`),
+ active: navLink(docsHref(version, `handbook/${p.slug}`)),
})),
});
}
@@ -345,18 +364,18 @@ function DocsNav({
label: t("docs.endpoints"),
icon: Server,
items: reference.tags.map((tag) => ({
- href: `/docs/reference/endpoints/${tag.name}`,
+ href: docsHref(version, `reference/endpoints/${tag.name}`),
label: tag.name,
- active: navLink(`/docs/reference/endpoints/${tag.name}`),
+ active: navLink(docsHref(version, `reference/endpoints/${tag.name}`)),
})),
});
groups.push({
label: t("docs.schemas"),
icon: Library,
items: reference.schemas.map((s) => ({
- href: `/docs/reference/schemas/${s.name}`,
+ href: docsHref(version, `reference/schemas/${s.name}`),
label: s.name,
- active: navLink(`/docs/reference/schemas/${s.name}`),
+ active: navLink(docsHref(version, `reference/schemas/${s.name}`)),
})),
});
}
@@ -365,9 +384,7 @@ function DocsNav({
label: t("docs.releases"),
icon: Tag,
items: versions.map((v) => ({
- href: v.version === (version ?? versions[0]?.version) && version !== null
- ? `/docs/releases/${v.version}`
- : `/docs/releases/${v.version}`,
+ href: `/docs/releases/${v.version}`,
label: v.version,
active: navLink(`/docs/releases/${v.version}`),
})),
@@ -708,7 +725,7 @@ function ReleaseNoteView({ version }: { version: string }) {
{version}
;
+ return ;
}
// ---------------------------------------------------------------------------
@@ -803,9 +821,11 @@ export default function Docs() {
});
const { data: releases, error: releasesError } = useJson(`${DOCS_BASE}/index.json`);
- const { data: handbook, error: handbookError } = useJson(
- version === null ? `${DOCS_BASE}/handbook/index.json` : null,
- );
+
+ const activeRelease = releases?.find((r) => r.version === version) ?? null;
+ const handbookAvailable = version === null || activeRelease?.hasHandbook;
+ const handbookUrl = handbookAvailable ? `${DOCS_BASE}/${docsFile(version, "handbook/index.json")}` : null;
+ const { data: handbook, error: handbookError } = useJson(handbookUrl);
const isCurrentVersion =
version === null ||
@@ -814,7 +834,7 @@ export default function Docs() {
const refUrl = version === null
? `${DOCS_BASE}/reference.json`
- : releases?.find((r) => r.version === version)?.hasReference
+ : activeRelease?.hasReference
? `${DOCS_BASE}/versions/${version}.json`
: null;
@@ -832,20 +852,13 @@ export default function Docs() {
return null;
}, [releases, versionInfo]);
- // Redirect old-style /docs/vX.Y.Z to /docs/releases/vX.Y.Z
- useEffect(() => {
- if (version !== null && path.length === 0) {
- setLocation(`/docs/releases/${version}`, { replace: true });
- }
- }, [version, path, setLocation]);
-
const handleVersionChange = (v: string | null) => {
setSearchQuery("");
if (v === null || v === currentVersion) {
setLocation("/docs");
return;
}
- setLocation(`/docs/releases/${v}`);
+ setLocation(`/docs/${v}`);
};
// ---- route resolution ----
@@ -864,7 +877,12 @@ export default function Docs() {
let content: React.ReactNode = null;
if (version !== null && path.length === 0) {
- content = ;
+ content =
+ handbook && handbook.length > 0 ? (
+
+ ) : (
+
+ );
} else if (section === "home") {
content =
handbook && handbook.length > 0 ? (
@@ -913,7 +931,7 @@ export default function Docs() {
{reference.tags.map((tg) => (
{tg.name}
@@ -930,7 +948,7 @@ export default function Docs() {
{reference.schemas.map((s) => (
{s.name}
@@ -955,75 +973,77 @@ export default function Docs() {
const showSearch = version === null && section !== "releases";
return (
-
-
-
-
-
-
-
-
-
- {t("docs.nav")}
+
+
+
+
+
+
+
+
+
+
+ {t("docs.nav")}
+
+
+
+
+
+
toolr
+
+
+ {t("docs.backToApp")}
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSearch &&
setSearchQuery("")} />}
+
+
+
+
{content}
-
-
-
-
-
-
-
-
-
- {showSearch &&
setSearchQuery("")} />}
-
-
-
+
);
}
diff --git a/artifacts/toolrate/src/pages/tool-detail.tsx b/artifacts/toolrate/src/pages/tool-detail.tsx
index 00dc42d..f3448b5 100644
--- a/artifacts/toolrate/src/pages/tool-detail.tsx
+++ b/artifacts/toolrate/src/pages/tool-detail.tsx
@@ -61,6 +61,7 @@ import {
import { customFetch } from "@workspace/api-client-react";
import { recordRecentTool } from "@/lib/recent-tools";
import { FieldHelp } from "@/components/field-help";
+import { GuideHelp } from "@/components/guide-help";
const ratingSchema = z.object({
usefulness: z.number().min(1).max(5),
@@ -615,7 +616,10 @@ export default function ToolDetail() {