Contribute rewrite Phase 6: narrow-viewport right-panel drawer
Below 1280px the right-panel (chat + change-card panel) collapses into an off-canvas drawer toggled from a Chat button in the editor toolbar. Above 1280px the inline three-column layout is unchanged. Replaces the Phase 2 narrow-viewport-banner stopgap. - drawerOpen useState in RFCView; per-session, default closed; tied to a .rfc-body.drawer-open class plus .right-panel.drawer-open and the backdrop's data-open attribute. - ChatDrawerToggleButton in each editor-toolbar branch (Contribute, Discuss-with-accepted-changes, and a narrow-only fallback toolbar for the default state). Badge mirrors change-panel-header: pending AI changes + the buffered manual card. Hidden above 1280px via CSS. - .right-panel becomes position: fixed; top: 48px; right: 0; width: min(420px, 92vw); transform: translateX(100%) by default; slides in with .drawer-open. Backdrop sibling with opacity transition handles click-to-close. - Escape closes the drawer when open (useEffect listener gated on drawerOpen, so it's a no-op when closed). - Toolbar gets padding-right shifted to drawer-width when the drawer is open at narrow widths so the toggle stays clickable to close (otherwise it'd sit under the open drawer). - @media (min-width: 1280px) restores .right-panel to inline flex (360px, position: static), hides the backdrop, hides the toggle button, and hides the narrow-only toolbar fallback. - Removes the .narrow-viewport-banner JSX + its CSS block + its @media display:none rule. SPEC.md §8.1 gains a paragraph noting the narrow-viewport drawer collapse — explicit acknowledgment that the three-column shape adapts below ~1280px without re-architecting around responsive primitives. Verified in Vite preview sandbox at 1024px (drawer offscreen, backdrop opacity 0, toggle visible) and 1440px (inline three-column, drawer fixed-positioning reset, toggle hidden, backdrop display:none). Backend was down this session (proxy 502 on /api/me) so the cumulative Phase 2–5 end-to-end verification gap is still open; sandbox proof only, as called out in the phase prompt. Backend integration suite: 125 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -650,6 +650,13 @@ swaps both the document body and the right-pane chat thread together.
|
||||
The URL updates accordingly (`/rfc/<slug>/branches/<name>`), 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`
|
||||
|
||||
+69
-11
@@ -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 {
|
||||
|
||||
@@ -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 */}
|
||||
<div className="rfc-body">
|
||||
<div className={`rfc-body${drawerOpen ? ' drawer-open' : ''}`}>
|
||||
<div className="editor-area" onClick={handleEditorClick}>
|
||||
{branchParam === 'main' && (
|
||||
<div className="discuss-mode-banner">
|
||||
@@ -646,13 +669,7 @@ export default function RFCView({ viewer }) {
|
||||
creator or an arbiter can grant access.
|
||||
</div>
|
||||
)}
|
||||
{mode === 'contribute' && canContribute && (
|
||||
<div className="narrow-viewport-banner">
|
||||
The split-pane Contribute layout works best on screens at least
|
||||
1280px wide. Resize to a wider window for full rendering.
|
||||
</div>
|
||||
)}
|
||||
{mode === 'contribute' && canContribute && (
|
||||
{showContributeToolbar && (
|
||||
<div className="editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
@@ -666,10 +683,14 @@ export default function RFCView({ viewer }) {
|
||||
{changes.filter(c => c.state === 'accepted').length} accepted ·{' '}
|
||||
{pendingCount} pending
|
||||
</span>
|
||||
<ChatDrawerToggleButton
|
||||
open={drawerOpen}
|
||||
count={drawerBadgeCount}
|
||||
onToggle={() => setDrawerOpen(v => !v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{inDiscuss && branchParam !== 'main'
|
||||
&& changes.some(c => c.state === 'accepted') && (
|
||||
{showDiscussToolbar && (
|
||||
<div className="editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
@@ -682,6 +703,23 @@ export default function RFCView({ viewer }) {
|
||||
<span className="editor-toolbar-hint">
|
||||
{changes.filter(c => c.state === 'accepted').length} accepted on this branch
|
||||
</span>
|
||||
<ChatDrawerToggleButton
|
||||
open={drawerOpen}
|
||||
count={drawerBadgeCount}
|
||||
onToggle={() => setDrawerOpen(v => !v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!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.
|
||||
<div className="editor-toolbar editor-toolbar-narrow-only">
|
||||
<ChatDrawerToggleButton
|
||||
open={drawerOpen}
|
||||
count={drawerBadgeCount}
|
||||
onToggle={() => setDrawerOpen(v => !v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{reviewMode ? (
|
||||
@@ -745,7 +783,13 @@ export default function RFCView({ viewer }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="right-panel">
|
||||
<div
|
||||
className="right-panel-backdrop"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
aria-hidden="true"
|
||||
data-open={drawerOpen ? 'true' : 'false'}
|
||||
/>
|
||||
<div className={`right-panel${drawerOpen ? ' drawer-open' : ''}`} role="complementary">
|
||||
<ChatPanel
|
||||
messages={messages}
|
||||
threads={branchView.threads || []}
|
||||
@@ -853,6 +897,26 @@ export default function RFCView({ viewer }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ChatDrawerToggleButton({ open, count, onToggle }) {
|
||||
// Phase 6 — toolbar button that opens/closes the right-panel drawer
|
||||
// at narrow viewports. The badge mirrors the change-panel-header's
|
||||
// .badge styling; an aria-label gives screen readers the count. CSS
|
||||
// hides the button above 1280px where the right panel is inline.
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`btn-chat-drawer-toggle${open ? ' active' : ''}`}
|
||||
onClick={onToggle}
|
||||
aria-pressed={open}
|
||||
aria-label={`${open ? 'Close' : 'Open'} chat panel${count > 0 ? ` (${count} pending)` : ''}`}
|
||||
title={open ? 'Close chat panel' : 'Open chat panel'}
|
||||
>
|
||||
Chat
|
||||
{count > 0 && <span className="badge">{count}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function focusMessage(messageId) {
|
||||
const el = document.querySelector(`[data-message-id="${messageId}"]`)
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
|
||||
Reference in New Issue
Block a user