diff --git a/SPEC.md b/SPEC.md index 8886be7..2672f70 100644 --- a/SPEC.md +++ b/SPEC.md @@ -650,6 +650,13 @@ swaps both the document body and the right-pane chat thread together. The URL updates accordingly (`/rfc//branches/`), so a branch view is shareable and back/forward navigation works. +Below roughly 1280px wide the three-column shape adapts: the right +column collapses into an off-canvas drawer toggled from a Chat button +in the editor toolbar (with a pending-count badge), rather than the +columns being suppressed or stacked. Above the breakpoint the inline +three-column layout is unchanged. This is the pre-fancy adaptation; +responsive primitives for the catalog column are out of scope here. + ### 8.2 Default view on selection Selecting an active RFC opens the center column on the RFC's `main` diff --git a/frontend/src/App.css b/frontend/src/App.css index c33bde7..e3d911a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -555,16 +555,6 @@ border-radius: 2px; padding: 1px 2px; cursor: pointer; } -/* Narrow-viewport banner — Phase 2 punts the chat-drawer collapse to - Phase 6, so the split layout asks for ≥1280px. */ -.narrow-viewport-banner { - padding: 8px 16px; - background: #fef3c7; border-bottom: 1px solid #fcd34d; - color: #78350f; font-size: 12px; -} -@media (min-width: 1280px) { - .narrow-viewport-banner { display: none; } -} .cm-source-editor .cm-editor { flex: 1; min-height: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; @@ -688,11 +678,79 @@ /* ── Right panel (chat + change panel) ───────────────────────────────── */ +/* Phase 6 — narrow viewport (<1280px) defaults: the right panel is an + off-canvas drawer sliding in from the right. The header (.app-header) + is 48px tall and stays visible above the drawer so branch/breadcrumb + navigation remains accessible. The @media (min-width: 1280px) block + below restores the inline three-column layout. */ .right-panel { - width: 360px; flex-shrink: 0; + position: fixed; + top: 48px; right: 0; bottom: 0; + width: min(420px, 92vw); border-left: 1px solid #e5e5e5; display: flex; flex-direction: column; overflow: hidden; background: #fff; + transform: translateX(100%); + transition: transform 150ms ease; + z-index: 30; + box-shadow: -8px 0 24px rgba(0,0,0,0.08); +} +.right-panel.drawer-open { transform: translateX(0); } + +.right-panel-backdrop { + position: fixed; inset: 48px 0 0 0; + background: rgba(0,0,0,0.2); + z-index: 29; + opacity: 0; + pointer-events: none; + transition: opacity 150ms ease; +} +.right-panel-backdrop[data-open="true"] { + opacity: 1; + pointer-events: auto; +} + +/* Chat drawer toggle button — sits in the editor toolbar at narrow widths + and gets hidden above 1280px (where the right panel is always inline). */ +.btn-chat-drawer-toggle { + margin-left: auto; + display: inline-flex; align-items: center; gap: 6px; + background: #fff; color: #1a1a1a; + border: 1px solid #d4d4d4; border-radius: 5px; + padding: 4px 10px; font-size: 12px; font-weight: 600; cursor: pointer; +} +.btn-chat-drawer-toggle.active { background: #1a1a1a; color: #fff; border-color: #1a1a1a; } +.btn-chat-drawer-toggle .badge { + font-size: 10px; padding: 1px 6px; + background: #b45309; color: #fff; border-radius: 10px; +} +.btn-chat-drawer-toggle.active .badge { background: #fff; color: #1a1a1a; } + +/* When the drawer is open at narrow widths, scoot the editor toolbar's + content left so the Chat toggle button is not hidden under the drawer. + The toggle has margin-left: auto, so padding-right on the toolbar + effectively shifts it left by the drawer's footprint, keeping "click + toggle again to close" reachable. The @media (min-width: 1280px) block + below resets this. */ +.rfc-body.drawer-open .editor-toolbar { + padding-right: calc(min(420px, 92vw) + 8px); +} + +@media (min-width: 1280px) { + /* Restore inline three-column layout above the 1280px breakpoint. */ + .rfc-body.drawer-open .editor-toolbar { padding-right: 16px; } + .right-panel { + position: static; + top: auto; right: auto; bottom: auto; + width: 360px; flex-shrink: 0; + transform: none; + transition: none; + z-index: auto; + box-shadow: none; + } + .right-panel-backdrop { display: none; } + .btn-chat-drawer-toggle { display: none; } + .editor-toolbar-narrow-only { display: none; } } .chat-panel { diff --git a/frontend/src/components/RFCView.jsx b/frontend/src/components/RFCView.jsx index ec5b979..3b04bdc 100644 --- a/frontend/src/components/RFCView.jsx +++ b/frontend/src/components/RFCView.jsx @@ -117,6 +117,11 @@ export default function RFCView({ viewer }) { // {paragraphCount, deadline} — null when buffer empty const [manualCountdown, setManualCountdown] = useState(null) + // Phase 6 — narrow-viewport (<1280px) right-column collapse. The drawer + // chrome is rendered unconditionally; CSS hides it above 1280px where + // the right panel is always inline. Per-session, default closed. + const [drawerOpen, setDrawerOpen] = useState(false) + useEffect(() => { getRFC(slug).then(setEntry).catch(err => setError(err.message)) listModels(slug) @@ -441,6 +446,17 @@ export default function RFCView({ viewer }) { flushManualBuffer() }, [flushManualBuffer]) + // Phase 6 — Escape closes the drawer when open. Cheap and gated on + // drawerOpen so the listener attaches only while needed. The CSS hides + // the drawer chrome above 1280px regardless, so this is a no-op there + // because drawerOpen has no visible effect either way. + useEffect(() => { + if (!drawerOpen) return + const onKey = (e) => { if (e.key === 'Escape') setDrawerOpen(false) } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [drawerOpen]) + // ── Editor-click → focus matching change card ────────────────────────── const handleEditorClick = useCallback((e) => { const span = e.target.closest('[data-change-id]') @@ -494,6 +510,13 @@ export default function RFCView({ viewer }) { const inDiscuss = mode === 'discuss' const pendingCount = changes.filter(c => c.state === 'pending').length + // Phase 6 — count surfaced on the narrow-viewport drawer toggle. Mirrors + // the change-panel-header badge: pending AI changes + the manual card + // when one's buffered. + const drawerBadgeCount = pendingCount + (manualPending ? 1 : 0) + const showContributeToolbar = mode === 'contribute' && canContribute + const showDiscussToolbar = inDiscuss && branchParam !== 'main' + && changes.some(c => c.state === 'accepted') // §10.1: the Open PR affordance only surfaces on a non-main branch // that has commits ahead of main and no open PR already. @@ -625,7 +648,7 @@ export default function RFCView({ viewer }) { )} {/* Two columns: editor + chat */} -
+
{branchParam === 'main' && (
@@ -646,13 +669,7 @@ export default function RFCView({ viewer }) { creator or an arbiter can grant access.
)} - {mode === 'contribute' && canContribute && ( -
- The split-pane Contribute layout works best on screens at least - 1280px wide. Resize to a wider window for full rendering. -
- )} - {mode === 'contribute' && canContribute && ( + {showContributeToolbar && (
)} - {inDiscuss && branchParam !== 'main' - && changes.some(c => c.state === 'accepted') && ( + {showDiscussToolbar && (
+ )} + {!showContributeToolbar && !showDiscussToolbar && ( + // Phase 6 — when neither editing toolbar would render, surface + // a narrow-only toolbar carrying just the drawer toggle. CSS + // hides it above 1280px where the right panel is always inline. +
+ setDrawerOpen(v => !v)} + />
)} {reviewMode ? ( @@ -745,7 +783,13 @@ export default function RFCView({ viewer }) { )}
-
+
setDrawerOpen(false)} + aria-hidden="true" + data-open={drawerOpen ? 'true' : 'false'} + /> +
0 ? ` (${count} pending)` : ''}`} + title={open ? 'Close chat panel' : 'Open chat panel'} + > + Chat + {count > 0 && {count}} + + ) +} + function focusMessage(messageId) { const el = document.querySelector(`[data-message-id="${messageId}"]`) if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })