diff --git a/web/CLAUDE.md b/web/CLAUDE.md index f4d486d..cce24f8 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -507,6 +507,45 @@ modal covers the screen anyway, but this can be improved in a future pass file, which triggers the `react-refresh/only-export-components` lint warning. This is consistent with the pre-existing `useProjects.tsx` pattern. +## Mobile Knowledge Base + +On viewports narrower than `768px` (Tailwind `md` breakpoint) the Knowledge +Base tab replaces the always-visible sidebar with a slide-in sheet triggered by +the doc-title row. Desktop layout is unchanged. + +### Architecture + +| File | Role | +|---|---| +| `web/src/components/KnowledgeBase/MobileDocSheet.tsx` | Backdrop overlay + right-side slide-in panel (reuses `card-panel` and `animate-panel-slide-in` CSS classes). Renders `KnowledgeBaseSidebar` inside. Accepts all sidebar props plus `onClose: () => void`. Calls `onClose` after a doc is selected (intercepts `onSelect`). | +| `web/src/components/KnowledgeBase/KnowledgeBase.tsx` | Owns `isSheetOpen: boolean` state. Applies `hidden md:flex` to the sidebar wrapper. Renders `` when `isSheetOpen` is true. Passes `onOpenSelector={() => setIsSheetOpen(true)}` to `KnowledgeDocViewer`. | +| `web/src/components/KnowledgeBase/KnowledgeDocViewer.tsx` | Accepts `onOpenSelector?: () => void`. Renders a `md:hidden` trigger button at the top of the component showing the current doc name + chevron. Clicking it calls `onOpenSelector?.()`. Also forwards `onOpenSelector` to `KnowledgeDocEditor` when editing. | +| `web/src/components/KnowledgeBase/KnowledgeDocEditor.tsx` | Accepts `onOpenSelector?: () => void`. Renders the same `md:hidden` trigger button at the top. When no doc is provided, shows "Choose a document ›" as the prompt. | + +### Behaviour + +- **Desktop (≥ `md`):** Two-column flex layout. Sidebar always visible on the + left (`w-72`). Trigger row hidden (`md:hidden`). +- **Mobile (< `md`):** Viewer/editor takes full width. Sidebar hidden + (`hidden md:flex`). Trigger row visible at the top showing current doc name + + chevron, or "Choose a document ›" when no doc is selected. Tapping the trigger + sets `isSheetOpen = true`. + +### Closing the sheet + +The sheet closes on either of: +- Tap the dark backdrop (`onClick` on the `bg-black/50` overlay) +- Select any document (intercepted by `MobileDocSheet.handleSelect`) + +### CSS conventions + +`MobileDocSheet` uses `card-panel` and `animate-panel-slide-in` — the same +classes used by `CardPanel` — so z-index and animation stay consistent with +the rest of the app. No new CSS was added. + +The trigger button uses `style` props (CSS custom properties) rather than +Tailwind color classes, consistent with the project-wide convention. + ## Subtask parent navigation Subtask cards display their parent card ID as a clickable badge. Clicking it diff --git a/web/src/components/KnowledgeBase/KnowledgeBase.tsx b/web/src/components/KnowledgeBase/KnowledgeBase.tsx index dcd236e..b951af0 100644 --- a/web/src/components/KnowledgeBase/KnowledgeBase.tsx +++ b/web/src/components/KnowledgeBase/KnowledgeBase.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { api, errorMessage } from '../../api/client'; import { KnowledgeBaseSidebar } from './KnowledgeBaseSidebar'; import { KnowledgeDocViewer } from './KnowledgeDocViewer'; +import { MobileDocSheet } from './MobileDocSheet'; import { useUnsavedGuard } from './useUnsavedGuard'; import { useKnowledgeBaseData } from './useKnowledgeBaseData'; import { useKnowledgeRefreshStatus } from './useKnowledgeRefreshStatus'; @@ -47,6 +48,7 @@ export function KnowledgeBase({ project }: { project: string }) { const refreshStatus = useKnowledgeRefreshStatus(project); const [planModal, setPlanModal] = useState<{ repo: string; plan: RefreshPlan } | null>(null); + const [isSheetOpen, setIsSheetOpen] = useState(false); const reload = useCallback(async () => { if (selected) { @@ -149,7 +151,7 @@ export function KnowledgeBase({ project }: { project: string }) { return (
-
+
{summaryError && (
setIsSheetOpen(true)} refreshing={ refreshStatus.repos[selected.repo]?.state === 'planning' || refreshStatus.repos[selected.repo]?.state === 'running' @@ -216,6 +219,16 @@ export function KnowledgeBase({ project }: { project: string }) { )}
{modal} + {isSheetOpen && ( + setIsSheetOpen(false)} + /> + )} {planModal && ( import('@uiw/react-md-editor')); interface EditorProps { initialContent: string; + doc?: string; onCancel: () => void; onSave: (content: string, signal: AbortSignal) => Promise; onDirtyChange?: (dirty: boolean) => void; + onOpenSelector?: () => void; } const MIN_EDITOR_PX = 160; -export function KnowledgeDocEditor({ initialContent, onCancel, onSave, onDirtyChange }: EditorProps) { +export function KnowledgeDocEditor({ initialContent, doc, onCancel, onSave, onDirtyChange, onOpenSelector }: EditorProps) { const [content, setContent] = useState(initialContent); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); @@ -77,6 +79,30 @@ export function KnowledgeDocEditor({ initialContent, onCancel, onSave, onDirtyCh return (
+ {onOpenSelector && ( + + )}
void; refreshing?: boolean; onRefreshClick?: (repo: string) => void; + onOpenSelector?: () => void; } const docTitleGlyph = ( @@ -63,6 +64,7 @@ export function KnowledgeDocViewer({ onDirtyChange, refreshing, onRefreshClick, + onOpenSelector, }: ViewerProps) { const [editing, setEditing] = useState(false); const { theme } = useTheme(); @@ -71,6 +73,7 @@ export function KnowledgeDocViewer({ return ( { onDirtyChange?.(false); setEditing(false); @@ -84,6 +87,7 @@ export function KnowledgeDocViewer({ setEditing(false); }} onDirtyChange={onDirtyChange} + onOpenSelector={onOpenSelector} /> ); } @@ -98,6 +102,24 @@ export function KnowledgeDocViewer({ return (
+ {onOpenSelector && ( + + )}
void; + onRefreshClick?: (repo: string) => void; + refreshStatusByRepo?: Record; + onClose: () => void; +} + +export function MobileDocSheet({ + summary, + selected, + onSelect, + onRefreshClick, + refreshStatusByRepo, + onClose, +}: MobileDocSheetProps) { + const handleSelect = (sel: { repo: string; doc: string }) => { + onSelect(sel); + onClose(); + }; + + return ( + <> +