feat(mobile): collapse rail by default; tighten project page header layout
Build and Deploy Verso / deploy (push) Has been cancelled

Editor:
- Auto-collapse the file-tree rail on mobile so the editor+PDF panels
  occupy the full screen width instead of sharing it with a 15% sidebar.
  The user can still open the rail from the toolbar; the collapse only
  fires on first load or when switching to a mobile viewport.

Project page (Lumiere):
- Move the XS/M/L zoom buttons from the mobile filter-pill toolbar into
  a row with the New Project button, so neither is stranded alone.
- The search bar gets its own full-width row above the actions row.
- Desktop layout is unchanged (search + new-project stay side-by-side;
  zoom stays in the title row). `display:contents` makes the wrapper
  div transparent to the desktop flex container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-18 22:18:28 +00:00
parent 51d0314c1c
commit 7bc60a4e10
3 changed files with 68 additions and 35 deletions
@@ -41,7 +41,11 @@ function detectMobile() {
export default function MainLayout() {
const [resizing, setResizing] = useState(false)
const { resizing: railResizing } = useRailContext()
const {
resizing: railResizing,
handlePaneCollapse: collapseRail,
isOpen: railIsOpen,
} = useRailContext()
const {
togglePdfPane,
handlePdfPaneExpand,
@@ -65,6 +69,16 @@ export default function MainLayout() {
}
}, [])
// On mobile, collapse the file-tree rail so editor+PDF panels get full width.
useEffect(() => {
if (isMobile && railIsOpen) {
collapseRail()
}
// Only run on mount and when isMobile changes — don't re-collapse if the
// user manually re-opens the rail during the session.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile])
// verticalSplit is always vertical; sideBySide becomes vertical on mobile
const isVertical =
pdfLayout === 'verticalSplit' ||
@@ -370,7 +370,7 @@ export function ProjectListLumiere() {
))}
</div>
</div>
{/* Mobile-only: filter pills + zoom control */}
{/* Mobile-only: filter pills */}
<div className="d-flex d-md-none lumiere-mobile-toolbar">
<div
className="lumiere-mobile-filters"
@@ -389,8 +389,23 @@ export function ProjectListLumiere() {
</button>
))}
</div>
</div>
<div className="lumiere-header-actions">
<SearchForm
inputValue={searchText}
setInputValue={setSearchText}
filter={filter}
selectedTag={selectedTag}
/>
{/* On mobile: new project + zoom share a row below the search bar */}
<div className="lumiere-header-actions-row">
<NewProjectButton
id="lumiere-new-project-button"
showAddAffiliationWidget
/>
{/* Zoom control: on desktop it sits in the title row; on mobile it moves here */}
<div
className="lumiere-zoom-control lumiere-mobile-zoom"
className="lumiere-zoom-control lumiere-mobile-zoom d-md-none"
role="group"
aria-label={t('card_size')}
>
@@ -407,17 +422,6 @@ export function ProjectListLumiere() {
))}
</div>
</div>
<div className="lumiere-header-actions">
<SearchForm
inputValue={searchText}
setInputValue={setSearchText}
filter={filter}
selectedTag={selectedTag}
/>
<NewProjectButton
id="lumiere-new-project-button"
showAddAffiliationWidget
/>
</div>
</div>
{selectedProjects.length > 0 && (
@@ -341,6 +341,12 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
flex-wrap: wrap;
}
// On desktop the inner row wrapper is transparent — new project button flows
// naturally inside the flex row alongside the search form.
.lumiere-header-actions-row {
display: contents;
}
// Search bar — wide enough to show the full placeholder (relaxed on mobile)
form.project-search .form-control {
min-width: 360px;
@@ -894,25 +900,37 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
@media (max-width: 767px) {
// Stack the header vertically so nothing overflows the viewport width.
// Without this, lumiere-header-actions (flex-shrink:0) forces the flex
// container wider than the screen.
.lumiere-header {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
// Stack header-actions vertically: search on its own row, then new-project+zoom
.lumiere-header-actions {
width: 100%;
flex-shrink: 1;
min-width: 0;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
form.project-search {
flex: 1;
width: 100%;
min-width: 0;
}
}
// New project button + zoom buttons share a row below the search bar
.lumiere-header-actions-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
.new-project-dropdown {
flex: 1;
}
}
// Compact row: 2-line layout
// Row 1: [checkbox] [name+tags] [⋮ action]
// Row 2: [checkbox] [format · owner · date]
@@ -977,12 +995,9 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
}
}
// ── Mobile toolbar: filter tabs + zoom control ───────────────────────────
// Two-row layout: filter tabs on top (wrapping), zoom on bottom-right.
// flex-wrap:wrap on the filter container is unconditionally safe (no
// overflow-x:auto ancestor chain needed). flex:1 1 auto on each tab makes
// all tabs in the same row equal-width regardless of label length, so the
// rows align cleanly without producing page overflow on narrow screens.
// ── Mobile toolbar: filter tabs ──────────────────────────────────────────
// flex-wrap:wrap on the filter container makes all tabs in the same row
// equal-width regardless of label length.
.lumiere-mobile-toolbar {
flex-direction: column;
@@ -999,9 +1014,9 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
width: 100%;
}
// Zoom buttons when rendered inside lumiere-header-actions-row on mobile
.lumiere-mobile-zoom {
flex-shrink: 0;
align-self: flex-end;
}
// Filter tabs: equal-width per row, teal underline on active