import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useNavigate } from 'react-router' import { ArrowLeft, BookOpen, Check, ChevronDown, ClipboardPaste, Download, Maximize2, Minus, Plus, Redo2, Sparkles, Undo2, } from 'lucide-react' import { useJourneyStudio } from '../../pages/journeyStudio/useJourneyStudio' import { PAGE_PRESET_ORDER, PAGE_PRESETS } from './pagePresets' import { foliosOf } from './bookSheets' import { StudioSidebar } from './StudioSidebar' import { ProviderPicker } from '../Journey/JourneyDetailPageProviderPicker' import { useAuthStore } from '../../store/authStore' import { StudioCanvas } from './StudioCanvas' import { StudioInspector } from './StudioInspector' import { StudioWordmark } from './StudioWordmark' import { SaveIndicator } from './SaveIndicator' import { downloadSpread } from './spreadFile' import { StudioExport } from './StudioExport' import { PeerBadges } from './PeerBadges' import { TrimField } from './TrimField' import { Tooltip } from '../shared/Tooltip' import { useStudioStore } from '../../store/studioStore' import { StudioAssistantColumn, StudioAssistantToggle, StudioPageStage } from './StudioAssistantPanel' import { useStudioAssistant, type StudioAssistant } from './useStudioAssistant' import '../../styles/dashboard.css' import '../../styles/studio.css' import './bookFontFaces' import HelpButton from '../Help/HelpButton' /** * The Studio shell: top bar, page rail, workbench, inspector. * * It is a child route of the journey and portals itself over it, so the journey * stays mounted underneath and shows through the blurred 16px margin. That * margin is the whole point — you are meant to see that you never left. * * `.trek-dash` on the root is not decoration: every colour, radius and shadow in * studio.css resolves against those tokens, so Studio follows the user's theme * and accent without a second palette to keep in sync. */ export default function StudioShell() { const s = useJourneyStudio() const navigate = useNavigate() const [bookView, setBookView] = useState(true) const [exporting, setExporting] = useState(false) const assistant = useStudioAssistant({ journeyId: s.journeyId, flush: s.flushBook }) if (s.isMobile) { return createPortal(
{s.t('journey.studio.desktopOnly')}
{s.t('journey.studio.desktopOnlyHint')}
, document.body, ) } const originStyle = s.origin ? ({ '--st-ox': `${s.origin.x}px`, '--st-oy': `${s.origin.y}px` } as React.CSSProperties) : undefined return createPortal( <>
setExporting(true)} assistant={assistant} /> {/* The journey gallery's picker, inside Studio's own layer (#2271). */} {s.providerPick && s.journey && ( e.type !== 'skeleton' || e.title)} trips={s.journey.trips} existingAssetIds={new Set((s.journey.gallery ?? []).filter(p => p.asset_id).map(p => p.asset_id!))} initialEntryId={s.providerPick.entryId} onClose={s.closeProviderPick} onAdd={s.addFromProvider} /> )}
{exporting && s.doc && ( setExporting(false)} /> )}
, document.body, ) } type Studio = ReturnType function StudioBar({ s, bookView, setBookView, onExport, assistant, }: { s: Studio; bookView: boolean; setBookView: (v: boolean) => void; onExport: () => void assistant: StudioAssistant }) { return (
{/* The lockup, not a name field. The book is the journey's, and it was already titled after it — a second place to type the same name is a field whose only job is to be left alone. What the bar is short of is somewhere to say what this thing *is*. */}
{s.t('journey.studio.beta')}
s.loadDoc(s.acceptTheirs(current))} onKeepMine={s.keepMine} onRetry={() => { void s.saveNow() }} />
{/* The book's assistant, as a column beside the page; only where it can run for this user. */} {/* Studio has no navbar, so the help center's button rides on its own bar. */}
) } /** * Lay out again — this spread, or the whole book. * * A split button rather than two: they are the same action at two scopes, and * the spread is the one you want nine times out of ten. Rebuilding the whole * book is behind the chevron, where it cannot be hit by accident, and both are * ordinary undo steps. */ /** * Paste what was copied onto the page on screen (#2316). Beside undo and redo * because it is the same kind of thing, and there so the keyboard shortcut is * not the only way to find it. */ function PasteButton({ s }: { s: Studio }) { const clipboard = useStudioStore(st => st.clipboard) const paste = useStudioStore(st => st.paste) const label = s.t('journey.studio.paste') return ( ) } function AutoLayoutButton({ s }: { s: Studio }) { const [open, setOpen] = useState(false) const box = useRef(null) useEffect(() => { if (!open) return const onDown = (e: PointerEvent) => { if (!box.current?.contains(e.target as Node)) setOpen(false) } const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation(); setOpen(false) } } document.addEventListener('pointerdown', onDown) document.addEventListener('keydown', onKey, true) return () => { document.removeEventListener('pointerdown', onDown) document.removeEventListener('keydown', onKey, true) } }, [open]) return (
{open && (
)}
) } /** * The format picker. * * A native