lumiere: fix mobile overflow + restore tile view with XS/M/L zoom
Build and Deploy Verso / deploy (push) Successful in 1m42s

Overflow fix: ActionsCell (up to 9 icon buttons) was assigned to the
`auto` column in the compact row, blowing out the row to ~300px.
Replace with ActionsDropdown (single ⋮ button) on mobile via
d-md-none / d-none d-md-flex, same pattern as the classic table.

Tile view: remove the isMobile force to compact (effectiveScale=0).
Add a mobile-only toolbar row (d-flex d-md-none) combining the filter
pills and a new XS/M/L zoom control:
  - XS (scale=0)  → compact rows
  - M  (scale=1)  → 2 tiles per row  (CSS: repeat(2, 1fr))
  - L  (scale≥1.35) → 1 tile per row (CSS: 1fr, class --mobile-1col)

The --lum-card-scale inline var is suppressed on mobile so CSS media
query controls thumbnail height (0.85 for 2-col, 1.3 for 1-col).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-18 08:54:30 +00:00
parent a004f21688
commit 6f419477df
2 changed files with 103 additions and 32 deletions
@@ -41,6 +41,7 @@ import FormatCell from './table/cells/format-cell'
import OwnerCell from './table/cells/owner-cell' import OwnerCell from './table/cells/owner-cell'
import LastUpdatedCell from './table/cells/last-updated-cell' import LastUpdatedCell from './table/cells/last-updated-cell'
import ActionsCell from './table/cells/actions-cell' import ActionsCell from './table/cells/actions-cell'
import ActionsDropdown from './dropdown/actions-dropdown'
import InlineTags from './table/cells/inline-tags' import InlineTags from './table/cells/inline-tags'
import WelcomePageContent from './welcome-page-content' import WelcomePageContent from './welcome-page-content'
@@ -68,6 +69,12 @@ const ZOOM_OPTIONS: { value: ZoomLevel; label: string }[] = [
{ value: 1.35, label: 'M' }, { value: 1.35, label: 'M' },
{ value: 1.75, label: 'L' }, { value: 1.75, label: 'L' },
] ]
// Mobile offers 3 sizes: XS (rows), M (2 tiles), L (1 tile)
const MOBILE_ZOOM_OPTIONS: { value: ZoomLevel; label: string }[] = [
{ value: 0, label: 'XS' },
{ value: 1, label: 'M' },
{ value: 1.35, label: 'L' },
]
const ZOOM_STORAGE_KEY = 'lumiere-card-scale' const ZOOM_STORAGE_KEY = 'lumiere-card-scale'
function useLumiereCardScale(): [ZoomLevel, (z: ZoomLevel) => void] { function useLumiereCardScale(): [ZoomLevel, (z: ZoomLevel) => void] {
@@ -245,7 +252,13 @@ const ProjectCardCompact = memo(function ProjectCardCompact({
</div> </div>
</div> </div>
<div className="lumiere-compact-actions"> <div className="lumiere-compact-actions">
<ActionsCell project={project} /> {/* ⋮ dropdown on mobile (single button), icon strip on desktop */}
<div className="d-md-none">
<ActionsDropdown project={project} />
</div>
<div className="d-none d-md-flex">
<ActionsCell project={project} />
</div>
</div> </div>
</div> </div>
) )
@@ -271,8 +284,14 @@ export function ProjectListLumiere() {
selectFilter, selectFilter,
} = useProjectListContext() } = useProjectListContext()
// On mobile, always use the compact (XS) row view — cards overflow on small screens // On mobile: M option (scale=1) → 2 tiles/row; L (scale≥1.35) → 1 tile/row.
const effectiveScale = isMobile ? 0 : cardScale // CSS overrides the card grid columns on mobile; we only add a class for L.
const mobileIsL = isMobile && cardScale !== 0 && cardScale >= 1.35
const activeMobileZoom = cardScale === 0
? 0
: cardScale >= 1.35
? 1.35
: 1
const selectedTag = tags.find(tag => tag._id === selectedTagId) const selectedTag = tags.find(tag => tag._id === selectedTagId)
const allSelected = const allSelected =
@@ -351,19 +370,42 @@ export function ProjectListLumiere() {
))} ))}
</div> </div>
</div> </div>
{/* Mobile-only filter pills replacing the hidden sidebar */} {/* Mobile-only: filter pills + zoom control */}
<div className="d-flex d-md-none lumiere-mobile-filters" role="group" aria-label={t('filter_projects')}> <div className="d-flex d-md-none lumiere-mobile-toolbar">
{MOBILE_FILTERS.map(({ f, label }) => ( <div
<button className="lumiere-mobile-filters"
key={f} role="group"
type="button" aria-label={t('filter_projects')}
className={`lumiere-mobile-filter-pill${filter === f ? ' active' : ''}`} >
onClick={() => selectFilter(f)} {MOBILE_FILTERS.map(({ f, label }) => (
aria-pressed={filter === f} <button
> key={f}
{label} type="button"
</button> className={`lumiere-mobile-filter-pill${filter === f ? ' active' : ''}`}
))} onClick={() => selectFilter(f)}
aria-pressed={filter === f}
>
{label}
</button>
))}
</div>
<div
className="lumiere-zoom-control lumiere-mobile-zoom"
role="group"
aria-label={t('card_size')}
>
{MOBILE_ZOOM_OPTIONS.map(({ value, label }) => (
<button
key={value}
type="button"
className={`lumiere-zoom-btn${activeMobileZoom === value ? ' active' : ''}`}
onClick={() => setCardScale(value)}
aria-pressed={activeMobileZoom === value}
>
{label}
</button>
))}
</div>
</div> </div>
<div className="lumiere-header-actions"> <div className="lumiere-header-actions">
<SearchForm <SearchForm
@@ -410,12 +452,16 @@ export function ProjectListLumiere() {
<p className="lumiere-empty">{t('no_projects')}</p> <p className="lumiere-empty">{t('no_projects')}</p>
) : ( ) : (
<div <div
className={`lumiere-card-grid${effectiveScale === 0 ? ' lumiere-card-grid--compact' : ''}`} className={[
'lumiere-card-grid',
cardScale === 0 && 'lumiere-card-grid--compact',
mobileIsL && 'lumiere-card-grid--mobile-1col',
].filter(Boolean).join(' ')}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
style={effectiveScale === 0 ? {} : ({ '--lum-card-scale': effectiveScale } as React.CSSProperties)} style={cardScale === 0 || isMobile ? {} : ({ '--lum-card-scale': cardScale } as React.CSSProperties)}
> >
{visibleProjects.map(project => {visibleProjects.map(project =>
effectiveScale === 0 ? ( cardScale === 0 ? (
<ProjectCardCompact key={project.id} project={project} /> <ProjectCardCompact key={project.id} project={project} />
) : ( ) : (
<ProjectCard key={project.id} project={project} /> <ProjectCard key={project.id} project={project} />
@@ -870,12 +870,15 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
opacity: 1; opacity: 1;
} }
// ── Mobile compact row — 2-line layout ──────────────────────────────────── // ── Mobile layout overrides ────────────────────────────────────────────────
// On small screens the 6-column grid overflows. Switch to:
// Row 1: [checkbox] [name+tags] [actions]
// Row 2: [checkbox] [format · owner · date]
@media (max-width: 767px) { @media (max-width: 767px) {
// Compact row: 2-line layout
// Row 1: [checkbox] [name+tags] [⋮ action]
// Row 2: [checkbox] [format · owner · date]
// ActionsDropdown (single ⋮ button) is shown via d-md-none; ActionsCell
// (many icon buttons) is hidden on mobile, so the auto column stays tiny.
.lumiere-compact-row { .lumiere-compact-row {
grid-template-columns: 28px 1fr auto; grid-template-columns: 28px 1fr auto;
grid-template-rows: auto auto; grid-template-rows: auto auto;
@@ -896,6 +899,9 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
grid-column: 3; grid-column: 3;
grid-row: 1; grid-row: 1;
align-self: center; align-self: center;
opacity: 1;
.action-btn { opacity: 1; }
} }
// Meta wrapper becomes a flex row spanning the second line // Meta wrapper becomes a flex row spanning the second line
@@ -907,30 +913,49 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
grid-column: 2 / 4; grid-column: 2 / 4;
grid-row: 2; grid-row: 2;
} }
}
// Actions always visible — no hover on touch devices // Card tile grid: M = 2 tiles/row, L = 1 tile/row.
@media (hover: none) { // --lum-card-scale inline var is suppressed on mobile (isMobile=true in JS),
.lumiere-compact-actions .action-btn { // so the CSS custom property here wins for thumbnail sizing.
opacity: 1; .lumiere-card-grid:not(.lumiere-card-grid--compact) {
} grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
--lum-card-scale: 0.85;
&.lumiere-card-grid--mobile-1col {
grid-template-columns: 1fr;
--lum-card-scale: 1.3;
} }
} }
} }
// ── Mobile filter pills (replaces hidden sidebar on xs/sm) ──────────────── // ── Mobile toolbar: filter pills + zoom control ───────────────────────────
.lumiere-mobile-toolbar {
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.25rem 0 0.5rem;
flex-shrink: 0;
}
.lumiere-mobile-filters { .lumiere-mobile-filters {
overflow-x: auto; overflow-x: auto;
display: flex;
gap: 0.45rem; gap: 0.45rem;
padding: 0.25rem 0 0.5rem; flex: 1;
min-width: 0;
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
flex-shrink: 0;
width: 100%;
&::-webkit-scrollbar { display: none; } &::-webkit-scrollbar { display: none; }
} }
.lumiere-mobile-zoom {
flex-shrink: 0;
}
.lumiere-mobile-filter-pill { .lumiere-mobile-filter-pill {
flex-shrink: 0; flex-shrink: 0;
padding: 0.3rem 0.8rem; padding: 0.3rem 0.8rem;