feat(docs): standalone docs page, Hilfe nav entry, rewritten user guide

- docs route renders without the app shell (own header, back-to-app link,
  theme toggle)
- nav entry renamed to Hilfe (de) / Help (en)
- reference links now case-insensitive (schema/tag slugs)
- handbook rewritten as 17-page user guide with links into the API reference
- release notes v0.8.1 added
This commit is contained in:
opencode
2026-08-03 22:52:14 +02:00
parent 9ceb11e7f9
commit fdf2d741a1
24 changed files with 833 additions and 323 deletions
+51 -21
View File
@@ -3,11 +3,11 @@ import { Link, useLocation } from "wouter";
import { Marked } from "marked";
import DOMPurify from "dompurify";
import { useTranslation } from "react-i18next";
import { Layout } from "@/components/layout";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ThemeToggle } from "@/components/theme-toggle";
import {
Select,
SelectContent,
@@ -17,6 +17,7 @@ import {
} from "@/components/ui/select";
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
import {
ArrowLeft,
BookOpen,
CalendarDays,
ExternalLink,
@@ -27,6 +28,7 @@ import {
Search,
Server,
Tag,
Wrench,
type LucideIcon,
} from "lucide-react";
@@ -485,15 +487,16 @@ function FieldTypeChip({ type }: { type: FieldType }) {
}
function FieldTable({ fields }: { fields: Field[] }) {
const { t } = useTranslation();
return (
<div className="overflow-x-auto rounded-lg border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50 text-left text-xs uppercase tracking-wider text-muted-foreground">
<th className="px-3 py-2 font-semibold">Feld</th>
<th className="px-3 py-2 font-semibold">Typ</th>
<th className="px-3 py-2 font-semibold">Pflicht</th>
<th className="px-3 py-2 font-semibold">Beschreibung</th>
<th className="px-3 py-2 font-semibold">{t("docs.field")}</th>
<th className="px-3 py-2 font-semibold">{t("docs.type")}</th>
<th className="px-3 py-2 font-semibold">{t("docs.required")}</th>
<th className="px-3 py-2 font-semibold">{t("docs.description")}</th>
</tr>
</thead>
<tbody>
@@ -529,6 +532,7 @@ function FieldTable({ fields }: { fields: Field[] }) {
}
function SchemaView({ schema }: { schema: SchemaModel }) {
const { t } = useTranslation();
const headings: Heading[] = schema.fields.map((f) => ({ id: f.name, text: f.name, level: 2 }));
return (
<div className="flex flex-col lg:flex-row gap-8">
@@ -540,15 +544,16 @@ function SchemaView({ schema }: { schema: SchemaModel }) {
<FieldTable fields={schema.fields} />
<p className="text-xs text-muted-foreground">
<HelpCircle className="h-3.5 w-3.5 inline mr-1" />
Hinweis: Formular-Felder verlinken per ?-Icon direkt zu den jeweiligen Zeilen dieser Tabelle.
{t("docs.fieldHelpHint")}
</p>
</div>
<Toc headings={headings} title="Felder" />
<Toc headings={headings} title={t("docs.fields")} />
</div>
);
}
function EndpointTagView({ tag }: { tag: TagGroup }) {
const { t } = useTranslation();
const headings: Heading[] = tag.endpoints.map((e) => ({
id: e.operationId,
text: `${e.method} ${e.path}`,
@@ -577,16 +582,16 @@ function EndpointTagView({ tag }: { tag: TagGroup }) {
{ep.parameters.length > 0 && (
<div className="mb-3">
<p className="mb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Parameter</p>
<p className="mb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground">{t("docs.parameter")}</p>
<div className="overflow-x-auto rounded-lg border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50 text-left text-xs uppercase tracking-wider text-muted-foreground">
<th className="px-3 py-2 font-semibold">Name</th>
<th className="px-3 py-2 font-semibold">In</th>
<th className="px-3 py-2 font-semibold">Typ</th>
<th className="px-3 py-2 font-semibold">Pflicht</th>
<th className="px-3 py-2 font-semibold">Beschreibung</th>
<th className="px-3 py-2 font-semibold">{t("docs.type")}</th>
<th className="px-3 py-2 font-semibold">{t("docs.required")}</th>
<th className="px-3 py-2 font-semibold">{t("docs.description")}</th>
</tr>
</thead>
<tbody>
@@ -623,9 +628,9 @@ function EndpointTagView({ tag }: { tag: TagGroup }) {
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50 text-left text-xs uppercase tracking-wider text-muted-foreground">
<th className="px-3 py-2 font-semibold">Status</th>
<th className="px-3 py-2 font-semibold">Beschreibung</th>
<th className="px-3 py-2 font-semibold">Schema</th>
<th className="px-3 py-2 font-semibold">{t("docs.status")}</th>
<th className="px-3 py-2 font-semibold">{t("docs.description")}</th>
<th className="px-3 py-2 font-semibold">{t("docs.schema")}</th>
</tr>
</thead>
<tbody>
@@ -642,7 +647,7 @@ function EndpointTagView({ tag }: { tag: TagGroup }) {
</section>
))}
</div>
<Toc headings={headings} title="Endpunkte" />
<Toc headings={headings} title={t("docs.endpoints")} />
</div>
);
}
@@ -737,12 +742,13 @@ function useDocsSearch(query: string) {
}
function SearchOverlay({ query, onClose }: { query: string; onClose: () => void }) {
const { t } = useTranslation();
const { results } = useDocsSearch(query);
if (!query.trim()) return null;
return (
<div className="mt-3 rounded-lg border bg-card p-2 shadow-md max-h-96 overflow-auto">
{results.length === 0 ? (
<p className="px-3 py-2 text-sm text-muted-foreground">Keine Treffer</p>
<p className="px-3 py-2 text-sm text-muted-foreground">{t("docs.noResults")}</p>
) : (
results.map((r) => (
<Link
@@ -849,7 +855,9 @@ export default function Docs() {
} else if (section === "handbook" && param) {
content = <HandbookView slug={param} />;
} else if (section === "reference" && param === "endpoints" && path[2]) {
const tag = reference?.tags.find((tg) => tg.name === path[2]);
const tag = reference?.tags.find(
(tg) => tg.name.toLowerCase() === path[2].toLowerCase(),
);
content = tag ? (
<EndpointTagView tag={tag} />
) : refError || (reference && !tag) ? (
@@ -858,7 +866,9 @@ export default function Docs() {
<Skeleton className="h-64 w-full" />
);
} else if (section === "reference" && param === "schemas" && path[2]) {
const schema = reference?.schemas.find((s) => s.name === path[2]);
const schema = reference?.schemas.find(
(s) => s.name.toLowerCase() === path[2].toLowerCase(),
);
content = schema ? (
<SchemaView schema={schema} />
) : refError || (reference && !schema) ? (
@@ -923,8 +933,28 @@ export default function Docs() {
const showSearch = version === null && section !== "releases";
return (
<Layout>
<div className="mx-auto max-w-6xl space-y-6 pb-10">
<div className="min-h-screen bg-background flex flex-col">
<header className="border-b bg-card shrink-0">
<div className="mx-auto max-w-6xl px-4 md:px-6 h-14 flex items-center justify-between gap-3">
<Link
href="/"
className="inline-flex items-center gap-2 text-primary font-bold text-lg min-w-0"
data-testid="link-docs-back"
>
<Wrench className="w-5 h-5 shrink-0" />
<span className="truncate">toolr</span>
<span className="hidden md:inline-flex items-center gap-1 text-xs font-normal text-muted-foreground border-l pl-2 ml-1">
<ArrowLeft className="w-3.5 h-3.5" />
{t("docs.backToApp")}
</span>
</Link>
<div className="flex items-center gap-1.5 shrink-0">
<ThemeToggle />
</div>
</div>
</header>
<div className="mx-auto max-w-6xl w-full flex-1 space-y-6 px-4 md:px-6 py-6 pb-12">
<DocsHeader
versions={releases ?? []}
activeVersion={version}
@@ -948,6 +978,6 @@ export default function Docs() {
<div className="min-w-0">{content}</div>
</div>
</div>
</Layout>
</div>
);
}