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' ;
1312import { 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 */
3427export 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,45 @@ 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 }
75+ </ div >
76+ ) : card . subtitle ? (
77+ < div className = "mt-0.5 truncate font-mono text-[11px] text-muted-foreground/70" >
78+ { card . subtitle }
12679 </ div >
12780 ) : null }
81+ < div className = "mt-1 text-xs text-muted-foreground/80" >
82+ { card . docCount } { card . docCount === 1 ? 'article' : 'articles' }
83+ </ div >
12884 </ div >
12985 </ Link >
13086 </ li >
0 commit comments