Skip to content

Commit 946cbbc

Browse files
os-zhuangclaude
andcommitted
feat(console): make the app-scoped docs index book-driven too
AppDocsIndex (/apps/:appName/docs) listed the package's docs flat; bring it in line with the portal. It now reads the shared book/doc data from the docs layout and shows the package's book(s): the common single-book case lands straight on that book's grouped reader (redirect), several books list as cards. Links carry the /apps/:appName prefix throughout. Browser-verified inside the showcase app container: /apps/com.example.showcase/docs redirects to .../docs/manual and renders the resolved spine with app-scoped doc links. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c5235ab commit 946cbbc

1 file changed

Lines changed: 36 additions & 84 deletions

File tree

apps/console/src/pages/AppDocsIndex.tsx

Lines changed: 36 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,77 +6,33 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import { useEffect, useState } from 'react';
10-
import { Link, useParams } from 'react-router-dom';
11-
import { AlertCircle, BookOpen, Loader2 } from 'lucide-react';
12-
import { useAdapter } from '@object-ui/app-shell';
9+
import { useMemo } from 'react';
10+
import { Link, Navigate, useParams } from 'react-router-dom';
11+
import { BookOpen, Loader2 } from 'lucide-react';
1312
import { DocShell } from './DocShell';
14-
15-
interface AppDocItem {
16-
name: string;
17-
label?: string;
18-
description?: string;
19-
}
13+
import { useBookData } from './use-book-data';
14+
import { buildBookCards, pkgOfBook, type Book } from './book-nav';
2015

2116
/**
22-
* `/apps/:appName/docs` — the app-scoped documentation index (ADR-0046/0048).
17+
* `/apps/:appName/docs` — the app-scoped documentation index (ADR-0046 §6 /
18+
* ADR-0048). The package-scoped sibling of the platform `/docs` portal: it
19+
* shows the books owned by the app whose container this route renders inside
20+
* (`:appName` is the package-id segment).
2321
*
24-
* The platform `/docs` portal lists *every* installed doc grouped by package;
25-
* this is its package-scoped sibling: the docs owned by the app whose
26-
* container this route renders inside (`:appName` is the package-id segment,
27-
* matched against each doc's `_packageId`). It is the landing target for the
28-
* header Help menu's "This app's docs" entry when an app ships more than one
29-
* doc — a single-doc app deep-links straight to the viewer instead.
30-
*
31-
* Like the rest of the doc routes it degrades softly: an app with no docs
32-
* shows an empty-state notice, never a hard error.
22+
* Book-driven like the portal: a package always has at least its implicit book
23+
* (§6.4). The common single-book case lands straight on that book's grouped
24+
* reader; a package with several books lists them. Reads the shared book/doc
25+
* data provided by the `/docs` layout — no separate fetch.
3326
*/
3427
export default function AppDocsIndex() {
35-
// `appName` is the parent route's package-id segment (/apps/:appName/docs).
3628
const { appName } = useParams<{ appName?: string }>();
37-
const adapter = useAdapter();
38-
const [docs, setDocs] = useState<AppDocItem[]>([]);
39-
const [state, setState] = useState<'loading' | 'ready' | 'error'>('loading');
40-
const [errorMessage, setErrorMessage] = useState<string>('');
29+
const { books, docs, state } = useBookData();
4130

42-
useEffect(() => {
43-
let cancelled = false;
44-
async function load() {
45-
if (!adapter) return;
46-
const client: any = adapter.getClient();
47-
if (!client?.meta?.getItems) {
48-
setErrorMessage('meta.getItems is not available on this client');
49-
setState('error');
50-
return;
51-
}
52-
setState('loading');
53-
try {
54-
const result: any = await client.meta.getItems('doc');
55-
const items: any[] = Array.isArray(result)
56-
? result
57-
: Array.isArray(result?.items)
58-
? result.items
59-
: Array.isArray(result?.value)
60-
? result.value
61-
: [];
62-
if (cancelled) return;
63-
const mine = items
64-
.filter((it) => it && it.name && it._packageId === appName)
65-
.map((it) => ({ name: it.name, label: it.label, description: it.description }))
66-
.sort((a, b) => (a.label ?? a.name).localeCompare(b.label ?? b.name));
67-
setDocs(mine);
68-
setState('ready');
69-
} catch (err: any) {
70-
if (cancelled) return;
71-
setErrorMessage(err?.message ?? 'Failed to load documentation');
72-
setState('error');
73-
}
74-
}
75-
void load();
76-
return () => {
77-
cancelled = true;
78-
};
79-
}, [adapter, appName]);
31+
const myBooks = useMemo<Book[]>(
32+
() => books.filter((b) => pkgOfBook(b) === appName),
33+
[books, appName],
34+
);
35+
const cards = useMemo(() => buildBookCards(myBooks, docs), [myBooks, docs]);
8036

8137
if (state === 'loading') {
8238
return (
@@ -86,45 +42,41 @@ export default function AppDocsIndex() {
8642
);
8743
}
8844

89-
if (state === 'error') {
90-
return (
91-
<div className="mx-auto flex max-w-3xl flex-col items-center gap-3 p-10 text-center">
92-
<AlertCircle className="h-10 w-10 text-destructive" />
93-
<h1 className="text-lg font-semibold">Failed to load documentation</h1>
94-
<p className="text-sm text-muted-foreground">{errorMessage}</p>
95-
</div>
96-
);
45+
// One book (the usual case) → land directly on its grouped reader.
46+
if (cards.length === 1) {
47+
return <Navigate to={`/apps/${appName}/docs/${cards[0].slug}`} replace />;
9748
}
9849

9950
return (
10051
<DocShell>
10152
<div className="mx-auto max-w-3xl p-4 sm:p-6">
102-
{docs.length === 0 ? (
53+
{cards.length === 0 ? (
10354
<div className="flex flex-col items-center gap-3 py-16 text-center text-muted-foreground">
10455
<BookOpen className="h-10 w-10" />
105-
<p className="text-sm">
106-
This app does not ship any documentation yet.
107-
</p>
56+
<p className="text-sm">This app does not ship any documentation yet.</p>
10857
<Link to="/docs" className="text-sm font-medium text-primary hover:underline">
10958
Browse all documentation
11059
</Link>
11160
</div>
11261
) : (
113-
<ul className="divide-y divide-border rounded-md border border-border">
114-
{docs.map((doc) => (
115-
<li key={doc.name}>
62+
<ul className="grid gap-3 sm:grid-cols-2">
63+
{cards.map((card) => (
64+
<li key={card.name}>
11665
<Link
117-
to={`/apps/${appName}/docs/${doc.name}`}
118-
className="flex items-start gap-3 px-3 py-3 hover:bg-muted/50"
66+
to={`/apps/${appName}/docs/${card.slug}`}
67+
className="flex h-full items-start gap-3 rounded-md border border-border p-3 hover:bg-muted/50"
11968
>
120-
<BookOpen className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
69+
<BookOpen className="mt-0.5 h-5 w-5 shrink-0 text-muted-foreground" />
12170
<div className="min-w-0">
122-
<div className="text-sm font-medium">{doc.label ?? doc.name}</div>
123-
{doc.description ? (
71+
<div className="text-sm font-semibold">{card.label}</div>
72+
{card.description ? (
12473
<div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
125-
{doc.description}
74+
{card.description}
12675
</div>
12776
) : null}
77+
<div className="mt-1 text-xs text-muted-foreground/80">
78+
{card.docCount} {card.docCount === 1 ? 'article' : 'articles'}
79+
</div>
12880
</div>
12981
</Link>
13082
</li>

0 commit comments

Comments
 (0)