Skip to content

Commit 9340078

Browse files
committed
refactor: make docs a real remote space instead of special case
Remove the entire separate docs rendering path (activeSpace === 'docs', docs-specific sidebar, banner, content rendering) and make docs a normal remote space in the manifest. handleOpenDocs now: 1. Calls ensureDocsSpace() which creates a remote space entry for github.com/nsheaps/cept (docs/content subpath) if it doesn't exist 2. Clones from GitHub or falls back to bundled DOCS_PAGES/DOCS_CONTENT 3. Switches to the space via the standard loadAndApplySpaceState flow This means docs now gets the same UI as any other remote space: - Top nav buttons (GitHub link, page menu) - Settings panel with Remote URL, Branch, Last synced - Refresh button to re-clone from remote - Standard sidebar with space switcher Removed: - activeSpace state and all docs/user branching - docsPages, docsContents, docsSource, docsLoading state - handleDocsPageSelect, handleDocsPageToggle callbacks - Docs-specific sidebar with read-only handlers - Docs-specific content area with banner and CeptEditor - DOCS_SPACE_INFO constant usage - 'cept-docs' exclusion from SettingsModal refresh button Closes #68 https://claude.ai/code/session_01TF6rYpGkqBWjzNQHgqrVmD
1 parent c328e5e commit 9340078

5 files changed

Lines changed: 145 additions & 353 deletions

File tree

packages/ui/src/components/App.tsx

Lines changed: 40 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import type { SearchResult } from './search/SearchPanel.js';
1212
import { PageHeader } from './page-header/PageHeader.js';
1313
import { SettingsModal, DEFAULT_SETTINGS } from './settings/SettingsModal.js';
1414
import 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';
1816
import {
1917
useStorage,
2018
useWorkspacePersistence,
@@ -46,7 +44,7 @@ import type { SpacesManifest } from './storage/SpaceManager.js';
4644
import { cloneRemoteRepo, normalizeRepoUrl } from './storage/git-space.js';
4745
import { BrowserFsBackend } from '@cept/core';
4846
import 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';
5048
import { 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 &mdash; {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

Comments
 (0)