@@ -12,9 +12,7 @@ import type { SearchResult } from './search/SearchPanel.js';
1212import { PageHeader } from './page-header/PageHeader.js' ;
1313import { SettingsModal , DEFAULT_SETTINGS } from './settings/SettingsModal.js' ;
1414import type { CeptSettings , SpaceInfo } from './settings/SettingsModal.js' ;
15- import { DOCS_PAGES , DOCS_CONTENT , DOCS_SPACE_INFO , resolveDocsContent } from './docs/docs-content.js' ;
16- import { loadDocs , getRemoteDocsSourceUrl } from './docs/docs-loader.js' ;
17- import type { DocsLoadResult } from './docs/docs-loader.js' ;
15+ import { ensureDocsSpace } from './docs/docs-loader.js' ;
1816import {
1917 useStorage ,
2018 useWorkspacePersistence ,
@@ -46,7 +44,7 @@ import type { SpacesManifest } from './storage/SpaceManager.js';
4644import { cloneRemoteRepo , normalizeRepoUrl } from './storage/git-space.js' ;
4745import { BrowserFsBackend } from '@cept/core' ;
4846import type { GitHttp } from '@cept/core' ;
49- import { restoreRoute , replaceRoute , pushRoute , parseRoute , isRemoteSpaceId , setUseGitPrefix } from '../router.js' ;
47+ import { restoreRoute , replaceRoute , parseRoute , isRemoteSpaceId , setUseGitPrefix } from '../router.js' ;
5048import { NotFoundPage } from './not-found/NotFoundPage.js' ;
5149
5250
@@ -110,13 +108,6 @@ export function App() {
110108 const [ settingsOpen , setSettingsOpen ] = useState ( false ) ;
111109 const [ settingsTab , setSettingsTab ] = useState < 'settings' | 'about' | 'spaces' > ( 'settings' ) ;
112110 const [ addSpaceWizardOpen , setAddSpaceWizardOpen ] = useState ( false ) ;
113- const [ activeSpace , setActiveSpace ] = useState < 'user' | 'docs' > ( 'user' ) ;
114- const [ docsSelectedPageId , setDocsSelectedPageId ] = useState < string | undefined > ( 'docs-index' ) ;
115- const [ docsPages , setDocsPages ] = useState < PageTreeNode [ ] > ( DOCS_PAGES ) ;
116- const [ docsContents , setDocsContents ] = useState < Record < string , string > > ( DOCS_CONTENT ) ;
117- const [ docsSource , setDocsSource ] = useState < DocsLoadResult [ 'source' ] > ( 'bundled' ) ;
118- const [ docsLoading , setDocsLoading ] = useState ( false ) ;
119- const docsLoadedRef = useRef ( false ) ;
120111 const [ importDialogOpen , setImportDialogOpen ] = useState ( false ) ;
121112 const [ importSource , setImportSource ] = useState < ImportSource > ( 'notion' ) ;
122113 const [ exportDialogOpen , setExportDialogOpen ] = useState ( false ) ;
@@ -317,8 +308,8 @@ export function App() {
317308
318309 const route = restoreRoute ( ) ;
319310 if ( route . space === 'docs' ) {
320- setActiveSpace ( ' docs' ) ;
321- if ( route . pageId ) setDocsSelectedPageId ( route . pageId ) ;
311+ // / docs URL — open docs as a regular remote space
312+ void handleOpenDocs ( ) ;
322313 } else if ( route . spaceId && route . spaceId !== 'default' && route . spaceId !== userSpaceId ) {
323314 // URL points to a different space — try to switch to it
324315 void loadSpaces ( backend ) . then ( ( manifest ) => {
@@ -385,7 +376,7 @@ export function App() {
385376 const updatedManifest = await loadSpaces ( backend ) ;
386377 setSpacesManifest ( updatedManifest ) ;
387378 setUserSpaceId ( newSpace . id ) ;
388- setActiveSpace ( 'user' ) ;
379+
389380 setPages ( clonedPages ) ;
390381 setPageContents ( clonedContents ) ;
391382 setSelectedPageId ( clonedPages [ 0 ] ?. id ) ;
@@ -536,27 +527,23 @@ export function App() {
536527 if ( ! initializedRef . current || ! routeRestoredRef . current ) return ;
537528 if ( ! hasStarted ) return ;
538529
539- if ( activeSpace === 'docs' ) {
540- replaceRoute ( { space : 'docs' , pageId : docsSelectedPageId } ) ;
541- } else if ( selectedPageId ) {
530+ if ( selectedPageId ) {
542531 replaceRoute ( { space : 'user' , spaceId : userSpaceId , pageId : selectedPageId } ) ;
543532 } else if ( userSpaceId !== 'default' ) {
544533 replaceRoute ( { space : 'user' , spaceId : userSpaceId } ) ;
545534 } else {
546535 replaceRoute ( { space : 'user' , spaceId : 'default' } ) ;
547536 }
548- } , [ selectedPageId , activeSpace , userSpaceId , docsSelectedPageId , hasStarted ] ) ;
537+ } , [ selectedPageId , userSpaceId , hasStarted ] ) ;
549538
550539 // Listen for back/forward navigation (popstate)
551540 useEffect ( ( ) => {
552541 const handlePopState = ( ) => {
553542 setNotFound ( null ) ; // Clear 404 on navigation
554543 const route = parseRoute ( ) ;
555544 if ( route . space === 'docs' ) {
556- setActiveSpace ( 'docs' ) ;
557- if ( route . pageId ) setDocsSelectedPageId ( route . pageId ) ;
545+ void handleOpenDocs ( ) ;
558546 } else {
559- setActiveSpace ( 'user' ) ;
560547 // Resolve the route to account for subPath splitting
561548 const resolved = spacesManifest
562549 ? resolveRouteToSpace ( spacesManifest , route . spaceId , route . pageId )
@@ -896,8 +883,6 @@ export function App() {
896883 // Save current space state before switching
897884 saveCurrentSpaceState ( userSpaceId , pages , favorites , recentPages , selectedPageId , spaceName , pageContents ) ;
898885 setSpaceLoadError ( undefined ) ;
899- // Switch to user view (important when creating from docs view)
900- setActiveSpace ( 'user' ) ;
901886 void createSpaceInBackend ( backend , name ) . then ( ( newSpace ) => {
902887 void loadSpaces ( backend ) . then ( ( manifest ) => {
903888 setSpacesManifest ( manifest ) ;
@@ -956,34 +941,32 @@ export function App() {
956941 setSettingsOpen ( true ) ;
957942 } , [ ] ) ;
958943
959- const handleOpenDocs = useCallback ( ( ) => {
960- setActiveSpace ( 'docs' ) ;
961- // Select the first page — remote pages use file-path IDs, bundled use 'docs-index'
962- const firstPageId = docsPages . length > 0 ? docsPages [ 0 ] . id : 'docs-index' ;
963- setDocsSelectedPageId ( firstPageId ) ;
964- pushRoute ( { space : 'docs' , pageId : firstPageId } ) ;
965-
966- // Trigger remote load if not loaded yet
967- if ( ! docsLoadedRef . current && backend instanceof BrowserFsBackend ) {
968- docsLoadedRef . current = true ;
969- setDocsLoading ( true ) ;
970- loadDocs ( backend as BrowserFsBackend , false , ( msg ) => {
971- addToast ( msg , 'info' ) ;
972- } ) . then ( ( result ) => {
973- setDocsPages ( result . pages ) ;
974- setDocsContents ( result . pageContents ) ;
975- setDocsSource ( result . source ) ;
976- // Select the first page of the loaded docs
977- if ( result . pages . length > 0 ) {
978- setDocsSelectedPageId ( result . pages [ 0 ] . id ) ;
979- }
980- } ) . catch ( ( ) => {
981- // Already falls back to bundled inside loadDocs
982- } ) . finally ( ( ) => {
983- setDocsLoading ( false ) ;
944+ const handleOpenDocs = useCallback ( async ( ) => {
945+ if ( ! ( backend instanceof BrowserFsBackend ) ) return ;
946+
947+ // Save current space state before switching
948+ saveCurrentSpaceState ( userSpaceId , pages , favorites , recentPages , selectedPageId , spaceName , pageContents ) ;
949+
950+ setCloneStatus ( { active : true , message : 'Loading documentation...' } ) ;
951+ try {
952+ // Ensure the docs space exists (creates + clones on first call, no-op after)
953+ const docsId = await ensureDocsSpace ( backend , ( msg ) => {
954+ setCloneStatus ( { active : true , message : msg } ) ;
984955 } ) ;
956+
957+ // Reload manifest and switch to the docs space
958+ const manifest = await loadSpaces ( backend ) ;
959+ setSpacesManifest ( manifest ) ;
960+ setUserSpaceId ( docsId ) ;
961+
962+ const space = manifest . spaces . find ( ( s ) => s . id === docsId ) ;
963+ await loadAndApplySpaceState ( docsId , space ?. name ?? 'Cept Docs' ) ;
964+ setCloneStatus ( { active : false } ) ;
965+ } catch ( err ) {
966+ const message = err instanceof Error ? err . message : 'Failed to load docs' ;
967+ setCloneStatus ( { active : false , error : message } ) ;
985968 }
986- } , [ backend , docsPages , addToast ] ) ;
969+ } , [ backend , userSpaceId , pages , favorites , recentPages , selectedPageId , spaceName , pageContents , saveCurrentSpaceState , loadAndApplySpaceState ] ) ;
987970
988971 /** Handle "Add Space" from the remote repo form in the wizard. */
989972 const handleAddRemoteRepo = useCallback ( async ( config : RemoteSpaceConfig ) => {
@@ -1018,7 +1001,6 @@ export function App() {
10181001 try {
10191002 // Save current space state before switching
10201003 saveCurrentSpaceState ( userSpaceId , pages , favorites , recentPages , selectedPageId , spaceName , pageContents ) ;
1021- setActiveSpace ( 'user' ) ;
10221004
10231005 // Clone the remote repo and extract pages
10241006 const { pages : clonedPages , pageContents : clonedContents } = await cloneRemoteRepo (
@@ -1139,18 +1121,6 @@ export function App() {
11391121 setSpacesManifest ( updatedManifest ) ;
11401122 } , [ backend , userSpaceId ] ) ;
11411123
1142- const handleDocsPageSelect = useCallback ( ( id : string ) => {
1143- setDocsSelectedPageId ( id ) ;
1144- setDocsPages ( ( prev ) => expandToNode ( prev , id ) ) ;
1145- if ( window . innerWidth < 768 ) {
1146- setSidebarOpen ( false ) ;
1147- }
1148- } , [ ] ) ;
1149-
1150- const handleDocsPageToggle = useCallback ( ( id : string ) => {
1151- setDocsPages ( ( prev ) => toggleNode ( prev , id ) ) ;
1152- } , [ ] ) ;
1153-
11541124 // Track cached stats for inactive spaces (page count from workspace state)
11551125 const [ inactiveSpaceStats , setInactiveSpaceStats ] = useState < Record < string , { pageCount : number ; contentSize : number } > > ( { } ) ;
11561126
@@ -1236,30 +1206,22 @@ export function App() {
12361206 contentSize,
12371207 } ) ;
12381208 }
1239- list . push ( {
1240- ...DOCS_SPACE_INFO ,
1241- pageCount : Object . keys ( docsContents ) . length ,
1242- contentSize : Object . values ( docsContents ) . reduce ( ( sum , c ) => sum + c . length , 0 ) ,
1243- } ) ;
12441209 return list ;
1245- } , [ hasStarted , pages , pageContents , spaceName , spacesManifest , userSpaceId , backend . type , inactiveSpaceStats , docsContents ] ) ;
1210+ } , [ hasStarted , pages , pageContents , spaceName , spacesManifest , userSpaceId , backend . type , inactiveSpaceStats ] ) ;
12461211
12471212 /** Known Git hosting domains — only these produce "View on GitHub" links. */
12481213 const KNOWN_GIT_HOSTS = [ 'github.com' , 'gitlab.com' , 'bitbucket.org' ] ;
12491214
12501215 const currentGithubUrl = useMemo ( ( ) : string | undefined => {
1251- if ( activeSpace === 'docs' && docsSelectedPageId ) {
1252- return getRemoteDocsSourceUrl ( docsSelectedPageId , docsSource ) ;
1253- }
1254- if ( activeSpace !== 'user' || ! selectedPageId || ! isRemoteSpaceId ( userSpaceId ) ) return undefined ;
1216+ if ( ! selectedPageId || ! isRemoteSpaceId ( userSpaceId ) ) return undefined ;
12551217 const parsed = parseRemoteSpaceId ( userSpaceId ) ;
12561218 if ( ! parsed ) return undefined ;
12571219 // Only link to known Git hosting domains to prevent open redirect
12581220 const host = parsed . repo . split ( '/' ) [ 0 ] ;
12591221 if ( ! KNOWN_GIT_HOSTS . includes ( host ) ) return undefined ;
12601222 const subPath = parsed . subPath ? `${ parsed . subPath } /` : '' ;
12611223 return `https://${ parsed . repo } /blob/${ parsed . branch } /${ subPath } ${ selectedPageId } ` ;
1262- } , [ activeSpace , docsSelectedPageId , docsSource , selectedPageId , userSpaceId ] ) ;
1224+ } , [ selectedPageId , userSpaceId ] ) ;
12631225
12641226 const commandItems : CommandItem [ ] = useMemo ( ( ) => [
12651227 { id : 'new-page' , title : 'New Page' , icon : '\u{1F4C4}' , category : 'Pages' , action : ( ) => handlePageAdd ( ) } ,
@@ -1273,12 +1235,9 @@ export function App() {
12731235
12741236 const currentContent = selectedPageId ? ( pageContents [ selectedPageId ] ?? '' ) : '' ;
12751237 const contentLoaded = selectedPageId ? ( selectedPageId in pageContents ) : false ;
1276- const docsSelectedNode = docsSelectedPageId ? findNode ( docsPages , docsSelectedPageId ) : undefined ;
12771238 const selectedNode = selectedPageId ? findNode ( pages , selectedPageId ) : undefined ;
12781239 const showOnboarding = ! hasStarted ;
12791240
1280- const isDocsActive = activeSpace === 'docs' ;
1281-
12821241 // Show loading state while backend loads persisted data
12831242 if ( ! ready ) {
12841243 return (
@@ -1307,7 +1266,7 @@ export function App() {
13071266 ) }
13081267 < div className = "ml-auto" />
13091268 < AppMenu
1310- pageId = { activeSpace === 'user' ? selectedPageId : undefined }
1269+ pageId = { selectedPageId }
13111270 isFavorite = { selectedPageId ? favorites . some ( ( f ) => f . id === selectedPageId ) : false }
13121271 githubUrl = { currentGithubUrl }
13131272 onToggleFavorite = { handleToggleFavorite }
@@ -1323,7 +1282,7 @@ export function App() {
13231282 { sidebarOpen && (
13241283 < div className = "cept-sidebar-backdrop" onClick = { ( ) => setSidebarOpen ( false ) } data-testid = "sidebar-backdrop" />
13251284 ) }
1326- { sidebarOpen && activeSpace === 'user' && (
1285+ { sidebarOpen && (
13271286 < Sidebar
13281287 pages = { pages }
13291288 favorites = { favorites }
@@ -1349,91 +1308,11 @@ export function App() {
13491308 onSpaceRename = { ( name ) => handleSpaceRename ( userSpaceId , name ) }
13501309 spaces = { spaceInfoList . map ( ( s ) => ( { id : s . id , name : s . name } ) ) }
13511310 activeSpaceId = { userSpaceId }
1352- onSwitchSpace = { ( id ) => {
1353- if ( id === DOCS_SPACE_INFO . id ) {
1354- handleOpenDocs ( ) ;
1355- } else {
1356- handleSwitchSpace ( id ) ;
1357- }
1358- } }
1359- />
1360- ) }
1361- { sidebarOpen && isDocsActive && (
1362- < Sidebar
1363- pages = { docsPages }
1364- favorites = { [ ] }
1365- recentPages = { [ ] }
1366- trash = { [ ] }
1367- selectedPageId = { docsSelectedPageId }
1368- onPageSelect = { handleDocsPageSelect }
1369- onPageToggle = { handleDocsPageToggle }
1370- onPageAdd = { ( ) => { /* read-only */ } }
1371- onPageRename = { ( ) => { /* read-only */ } }
1372- onPageDuplicate = { ( ) => { /* read-only */ } }
1373- onPageDelete = { ( ) => { /* read-only */ } }
1374- onPageMoveToRoot = { ( ) => { /* read-only */ } }
1375- onToggleFavorite = { ( ) => { /* read-only */ } }
1376- onRestoreFromTrash = { ( ) => { /* read-only */ } }
1377- onPermanentDelete = { ( ) => { /* read-only */ } }
1378- onEmptyTrash = { ( ) => { /* read-only */ } }
1379- onSearch = { ( ) => setSearchOpen ( true ) }
1380- onOpenSettings = { handleOpenSettings }
1381- onOpenDocs = { handleOpenDocs }
1382- readOnly
1383- spaceName = { DOCS_SPACE_INFO . name }
1384- spaces = { spaceInfoList . map ( ( s ) => ( { id : s . id , name : s . name } ) ) }
1385- activeSpaceId = { DOCS_SPACE_INFO . id }
1386- onSwitchSpace = { ( id ) => {
1387- if ( id === DOCS_SPACE_INFO . id ) {
1388- handleOpenDocs ( ) ;
1389- } else {
1390- setActiveSpace ( 'user' ) ;
1391- handleSwitchSpace ( id ) ;
1392- }
1393- } }
1311+ onSwitchSpace = { handleSwitchSpace }
13941312 />
13951313 ) }
13961314 < section className = "flex-1 min-w-0 p-4 md:p-8 overflow-y-auto" >
1397- { isDocsActive ? (
1398- docsLoading ? (
1399- < div className = "text-center text-gray-400 mt-20" data-testid = "docs-loading" >
1400- < p > Loading documentation...</ p >
1401- </ div >
1402- ) : docsSelectedPageId && docsContents [ docsSelectedPageId ] ? (
1403- < >
1404- < div className = "cept-docs-banner" data-testid = "docs-banner" >
1405- < svg width = "14" height = "14" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.5" >
1406- < rect x = "2" y = "1" width = "12" height = "14" rx = "1" />
1407- < path d = "M5 5h6M5 8h6M5 11h3" />
1408- </ svg >
1409- < span >
1410- Read-only — { docsSource === 'bundled'
1411- ? 'sourced from bundled docs (offline fallback)'
1412- : docsSource === 'cache'
1413- ? 'sourced from cached Git clone'
1414- : 'sourced from docs/ in the Git repository' }
1415- </ span >
1416- </ div >
1417- < CeptEditor
1418- key = { `docs-${ docsSelectedPageId } ` }
1419- content = { docsSource === 'bundled' ? resolveDocsContent ( docsContents [ docsSelectedPageId ] ) : docsContents [ docsSelectedPageId ] }
1420- placeholder = ""
1421- onUpdate = { ( ) => { /* read-only */ } }
1422- editable = { false }
1423- />
1424- { docsSelectedNode && docsSelectedNode . children . length > 0 && (
1425- < FolderView
1426- children = { docsSelectedNode . children }
1427- onPageSelect = { handleDocsPageSelect }
1428- />
1429- ) }
1430- </ >
1431- ) : (
1432- < div className = "text-center text-gray-400 mt-20" >
1433- < p > Select a documentation page from the sidebar</ p >
1434- </ div >
1435- )
1436- ) : spaceLoadError ? (
1315+ { spaceLoadError ? (
14371316 < div className = "cept-space-error" data-testid = "space-load-error" >
14381317 < svg width = "32" height = "32" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.5" >
14391318 < circle cx = "8" cy = "8" r = "7" />
@@ -1468,8 +1347,7 @@ export function App() {
14681347 } }
14691348 onGoToDocs = { ( ) => {
14701349 setNotFound ( null ) ;
1471- setActiveSpace ( 'docs' ) ;
1472- pushRoute ( { space : 'docs' } ) ;
1350+ void handleOpenDocs ( ) ;
14731351 } }
14741352 onGoBack = { window . history . length > 1 ? ( ) => {
14751353 setNotFound ( null ) ;
0 commit comments