From 278c66897af051b8df123b3daf97874515c7d830 Mon Sep 17 00:00:00 2001 From: Iqbal Date: Thu, 29 Oct 2026 10:14:34 +0700 Subject: [PATCH] Make the sidebar collapsible and let the knowledge graph go full screen Co-Authored-By: Claude Opus 5.5 --- src/components/AppLayout.tsx | 34 +++++++++++++++++++++++++++------- src/components/Icon.tsx | 3 +++ src/i18n.ts | 12 ++++++++++-- src/pages/Production.tsx | 36 ++++++++++++++++++++++++++++++++++-- src/styles/theme.css | 30 ++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 11 deletions(-) diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 2622b38..09da81c 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -128,6 +128,12 @@ function OrgSwitcher() { ) } +// Remembered per browser; storage can throw in private windows, so it falls back to expanded. +const COLLAPSED_KEY = 'sidebar-collapsed' +function readCollapsed() { + try { return localStorage.getItem(COLLAPSED_KEY) === '1' } catch { return false } +} + export function AppLayout() { useLang() const [help, setHelp] = useState(false) @@ -138,16 +144,22 @@ export function AppLayout() { // Nav groups stay closed unless they hold the current page; a click on the label overrides that until the next route. const [toggled, setToggled] = useState>>({}) useEffect(() => setToggled({}), [pathname]) + const [collapsed, setCollapsed] = useState(readCollapsed) + const toggleCollapsed = () => setCollapsed(c => { + try { localStorage.setItem(COLLAPSED_KEY, c ? '0' : '1') } catch { /* keep it for this visit only */ } + return !c + }) const name = user?.fullName || user?.primaryEmailAddress?.emailAddress || '-' return ( -
+
+
{t('layout.breadcrumb')} / {current ? t(current.label) : ''}
{t('layout.engine')} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 04d79d0..c114aab 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -18,6 +18,9 @@ const paths: Record = { info: '', check: '', 'chevron-down': '', + expand: '', + shrink: '', + sidebar: '', 'chevron-updown': '', logout:'', plus: '', diff --git a/src/i18n.ts b/src/i18n.ts index ca01a32..8d65ab2 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -23,6 +23,8 @@ const en = { 'layout.toStudio': 'Switch to Learning Lab', 'layout.footer': 'A workspace for knowledge that keeps growing.', 'layout.mainNav': 'Main navigation', + 'layout.collapseSidebar': 'Collapse sidebar', + 'layout.expandSidebar': 'Expand sidebar', 'layout.language': 'Language', // Auth @@ -71,6 +73,8 @@ const en = { 'prod.newContent': 'Create new content', 'prod.graphTitle': 'Knowledge graph', 'prod.clickNode': 'Click a node', + 'prod.fullscreen': 'Full screen', + 'prod.exitFullscreen': 'Exit full screen', 'prod.loadingGraph': 'Drawing the knowledge graph…', 'prod.graphLabel': 'Knowledge graph of {nodes} nodes', 'prod.hub.sources': 'Sources', @@ -262,11 +266,11 @@ const en = { 'runErr.agent_not_active': 'The schedule is paused or deleted.', 'runErr.run_access_revoked': 'You no longer have permission to run this schedule.', 'runErr.required_tool_unavailable': 'A tool this schedule needs is turned off.', + 'sch.noNewSources': 'No new sources found. A more specific topic may help.', 'sch.blockedBy': 'Blocking material:', 'sch.blockedNone': 'All material is ready now. Try again.', 'sch.fixInStudio': 'Open in Learning Studio', 'issue.document_processing': 'still processing', - 'sch.noNewSources': 'No new sources found. A more specific topic may help.', 'issue.document_failed': 'failed to process; reprocess or delete it', 'issue.graph_building': 'knowledge graph still building', 'issue.graph_not_ready': 'knowledge graph not prepared yet', @@ -529,6 +533,8 @@ const id: Record = { 'prod.newContent': 'Buat konten baru', 'prod.graphTitle': 'Knowledge graph', 'prod.clickNode': 'Klik sebuah node', + 'prod.fullscreen': 'Layar penuh', + 'prod.exitFullscreen': 'Keluar layar penuh', 'prod.loadingGraph': 'Menggambar knowledge graph…', 'prod.graphLabel': 'Knowledge graph berisi {nodes} node', 'prod.hub.sources': 'Sumber', @@ -562,6 +568,8 @@ const id: Record = { 'layout.toStudio': 'Beralih ke Learning Lab', 'layout.footer': 'Ruang kerja untuk pengetahuan yang terus berkembang.', 'layout.mainNav': 'Navigasi utama', + 'layout.collapseSidebar': 'Ciutkan sidebar', + 'layout.expandSidebar': 'Lebarkan sidebar', 'layout.language': 'Bahasa', 'auth.loading': 'Memuat…', @@ -752,6 +760,7 @@ const id: Record = { 'runErr.agent_not_active': 'Jadwal sedang dijeda atau sudah dihapus.', 'runErr.run_access_revoked': 'Kamu tidak lagi punya izin menjalankan jadwal ini.', 'runErr.required_tool_unavailable': 'Alat yang dibutuhkan jadwal ini sedang nonaktif.', + 'sch.noNewSources': 'Tidak ada source baru yang ditemukan. Topik yang lebih spesifik bisa membantu.', 'sch.blockedBy': 'Materi yang menahan:', 'sch.blockedNone': 'Semua materi sudah siap. Coba lagi.', 'sch.fixInStudio': 'Buka di Learning Studio', @@ -760,7 +769,6 @@ const id: Record = { 'issue.graph_building': 'knowledge graph masih dibangun', 'issue.graph_not_ready': 'knowledge graph belum disiapkan', 'issue.legacy_set_needs_conversion': 'set Compose lama; perlu dikonversi', - 'sch.noNewSources': 'Tidak ada source baru yang ditemukan. Topik yang lebih spesifik bisa membantu.', 'issue.no_knowledge_sources': 'set Compose tidak punya sumber', 'issue.source_not_accessible': 'set Compose memakai dokumen yang tidak bisa kamu akses', 'issue.source_graph_not_ready': 'salah satu dokumen sumber belum siap', diff --git a/src/pages/Production.tsx b/src/pages/Production.tsx index 1960eb0..1603016 100644 --- a/src/pages/Production.tsx +++ b/src/pages/Production.tsx @@ -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 = { 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(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() { {t('nav.brain')} {t('prod.newContent')} } /> -
+
+ {graph ? :
{docs.isError || results.isError ? t('brain.engineDown') : t('prod.loadingGraph')}
} diff --git a/src/styles/theme.css b/src/styles/theme.css index f6ff264..c6e629e 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -377,8 +377,15 @@ dialog::backdrop{background:rgba(16,24,40,.45);backdrop-filter:blur(4px)} .graph-empty a,.graph-more{color:#84caff} .graph-more{display:block;font-size:12px;margin:8px 4px 0} +.graph-fullscreen{position:absolute;top:16px;right:316px;z-index:3;display:grid;place-items:center;width:34px;height:34px;padding:0;border:1px solid rgba(255,255,255,.14);border-radius:10px;background:rgba(10,22,40,.6);backdrop-filter:blur(8px);color:#d0d5dd;cursor:pointer;transition:background .15s,color .15s} +.graph-fullscreen:hover{background:rgba(46,144,250,.25);color:#fff} +.graph-fullscreen svg{width:16px;height:16px} +.graph-stage.full{border-radius:0;min-height:0;height:100vh;height:100dvh;box-shadow:none} +.graph-stage.pinned{position:fixed;inset:0;z-index:50} @media(max-width:900px){ .graph-stage{min-height:0;display:flex;flex-direction:column} + .graph-stage.full{overflow:auto} + .graph-fullscreen{right:16px} .graph-svg{position:relative;width:100%;height:auto;aspect-ratio:1000/640} .graph-loading{position:relative;inset:auto;min-height:280px} .graph-note{position:relative;top:auto;left:auto;margin:16px 16px 0;max-width:none;align-self:flex-start} @@ -395,3 +402,26 @@ dialog::backdrop{background:rgba(16,24,40,.45);backdrop-filter:blur(4px)} .activity-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px} .activity-empty{padding:28px 0;text-align:center} @media(max-width:1000px){.activity-grid{grid-template-columns:1fr}} + +/* Collapsible sidebar: the toggle in the topbar shrinks it to an icon rail (desktop only) */ +.sidebar,.workspace-main{transition:width .2s,margin-left .2s} +.sidebar-toggle{flex-shrink:0;margin-right:12px} +.topbar{justify-content:flex-start}.topbar-right{margin-left:auto} +.sidebar-help-icon{align-self:center;margin:auto 0 12px} +@media(min-width:701px){ + .sidebar-collapsed .sidebar{width:76px;padding-left:12px;padding-right:12px} + .sidebar-collapsed .workspace-main{margin-left:76px;width:calc(100% - 76px)} + .sidebar-collapsed .sidebar>a>.brand{margin:0 0 24px;justify-content:center} + .sidebar-collapsed .sidebar .brand>span:last-child, + .sidebar-collapsed .workspace>div,.sidebar-collapsed .workspace>svg, + .sidebar-collapsed .sidebar nav button.nav-label,.sidebar-collapsed .nav-text, + .sidebar-collapsed .profile>div{display:none} + .sidebar-collapsed button.workspace{justify-content:center;padding:8px} + .sidebar-collapsed .workspace-menu{right:auto;width:240px} + .sidebar-collapsed .sidebar nav :is(button,a){justify-content:center;padding:11px 0} + .sidebar-collapsed .sidebar nav .nav-items~.nav-items>:first-child{margin-top:12px} + .sidebar-collapsed .profile{flex-direction:column;padding-left:0;padding-right:0} + .sidebar-collapsed .profile>.icon-btn{margin-left:0} +} +@media(max-width:700px){.sidebar-toggle,.sidebar-help-icon{display:none}} +@media(prefers-reduced-motion:reduce){.sidebar,.workspace-main{transition:none}}