Make the sidebar collapsible and let the knowledge graph go full screen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-29 10:14:34 +07:00
parent bcd7964e0b
commit 278c66897a
5 changed files with 104 additions and 11 deletions

View File

@@ -1,4 +1,4 @@
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useUser } from '@clerk/react'
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
@@ -16,6 +16,32 @@ const HUB_LABEL: Record<Cluster, Key> = {
sources: 'prod.hub.sources', made: 'prod.hub.made', working: 'prod.hub.working', research: 'prod.hub.research', schedules: 'prod.hub.schedules',
}
// Uses the Fullscreen API on the whole stage (graph + panel); where it is missing (iPhone Safari)
// the stage is pinned over the page instead, and Escape leaves it.
function useFullscreen() {
const ref = useRef<HTMLElement>(null)
const [pinned, setPinned] = useState(false)
const [native, setNative] = useState(false)
useEffect(() => {
const sync = () => setNative(document.fullscreenElement != null && document.fullscreenElement === ref.current)
document.addEventListener('fullscreenchange', sync)
return () => document.removeEventListener('fullscreenchange', sync)
}, [])
useEffect(() => {
if (!pinned) return
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setPinned(false) }
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
}, [pinned])
const toggle = () => {
if (native) void document.exitFullscreen()
else if (pinned) setPinned(false)
else if (ref.current?.requestFullscreen && document.fullscreenEnabled) ref.current.requestFullscreen().catch(() => setPinned(true))
else setPinned(true)
}
return { ref, full: native || pinned, pinned, toggle }
}
const when = (iso: string) => new Intl.DateTimeFormat(locale(), { weekday: 'short', hour: '2-digit', minute: '2-digit' }).format(new Date(iso))
// The dashboard: what is in the Brain, what the AI made from it, what is still being made, and the
@@ -33,6 +59,8 @@ export function ProductionPage() {
})
const schedules = useQuery({ queryKey: ['schedules'], queryFn: listSchedules, refetchInterval: 30_000 })
const fullscreen = useFullscreen()
const materials = results.data?.materials ?? []
const working = materials.filter(isWorking)
const made = materials.filter(m => !isWorking(m) && m.status !== 'failed')
@@ -57,7 +85,11 @@ export function ProductionPage() {
<Link className="btn" to="/upload"><Icon name="upload" />{t('nav.brain')}</Link>
<Link className="btn primary" to="/brain"><Icon name="spark" />{t('prod.newContent')}</Link>
</>} />
<section className="graph-stage" aria-label={t('prod.graphTitle')}>
<section ref={fullscreen.ref} className={`graph-stage${fullscreen.full ? ' full' : ''}${fullscreen.pinned ? ' pinned' : ''}`} aria-label={t('prod.graphTitle')}>
<button type="button" className="graph-fullscreen" onClick={fullscreen.toggle} aria-pressed={fullscreen.full}
aria-label={t(fullscreen.full ? 'prod.exitFullscreen' : 'prod.fullscreen')} title={t(fullscreen.full ? 'prod.exitFullscreen' : 'prod.fullscreen')}>
<Icon name={fullscreen.full ? 'shrink' : 'expand'} />
</button>
{graph
? <KnowledgeGraph graph={graph} />
: <div className="graph-loading" role="status">{docs.isError || results.isError ? t('brain.engineDown') : t('prod.loadingGraph')}</div>}