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 LastUpdatedCell from './table/cells/last-updated-cell'
import ActionsCell from './table/cells/actions-cell'
import ActionsDropdown from './dropdown/actions-dropdown'
import InlineTags from './table/cells/inline-tags'
import WelcomePageContent from './welcome-page-content'
@@ -68,6 +69,12 @@ const ZOOM_OPTIONS: { value: ZoomLevel; label: string }[] = [
{ value: 1.35, label: 'M' },
{ 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'
function useLumiereCardScale(): [ZoomLevel, (z: ZoomLevel) => void] {
@@ -245,9 +252,15 @@ const ProjectCardCompact = memo(function ProjectCardCompact({
</div>
</div>
<div className="lumiere-compact-actions">
{/* ⋮ 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>
)
})
@@ -271,8 +284,14 @@ export function ProjectListLumiere() {
selectFilter,
} = useProjectListContext()
// On mobile, always use the compact (XS) row view — cards overflow on small screens
const effectiveScale = isMobile ? 0 : cardScale
// On mobile: M option (scale=1) → 2 tiles/row; L (scale≥1.35) → 1 tile/row.
// 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 allSelected =
@@ -351,8 +370,13 @@ export function ProjectListLumiere() {
))}
</div>
</div>
{/* Mobile-only filter pills replacing the hidden sidebar */}
<div className="d-flex d-md-none lumiere-mobile-filters" role="group" aria-label={t('filter_projects')}>
{/* Mobile-only: filter pills + zoom control */}
<div className="d-flex d-md-none lumiere-mobile-toolbar">
<div
className="lumiere-mobile-filters"
role="group"
aria-label={t('filter_projects')}
>
{MOBILE_FILTERS.map(({ f, label }) => (
<button
key={f}
@@ -365,6 +389,24 @@ export function ProjectListLumiere() {
</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 className="lumiere-header-actions">
<SearchForm
inputValue={searchText}
@@ -410,12 +452,16 @@ export function ProjectListLumiere() {
<p className="lumiere-empty">{t('no_projects')}</p>
) : (
<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
style={effectiveScale === 0 ? {} : ({ '--lum-card-scale': effectiveScale } as React.CSSProperties)}
style={cardScale === 0 || isMobile ? {} : ({ '--lum-card-scale': cardScale } as React.CSSProperties)}
>
{visibleProjects.map(project =>
effectiveScale === 0 ? (
cardScale === 0 ? (
<ProjectCardCompact 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;
}
// ── Mobile compact row — 2-line layout ────────────────────────────────────
// On small screens the 6-column grid overflows. Switch to:
// Row 1: [checkbox] [name+tags] [actions]
// Row 2: [checkbox] [format · owner · date]
// ── Mobile layout overrides ────────────────────────────────────────────────
@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 {
grid-template-columns: 28px 1fr 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-row: 1;
align-self: center;
opacity: 1;
.action-btn { opacity: 1; }
}
// 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-row: 2;
}
// Actions always visible — no hover on touch devices
@media (hover: none) {
.lumiere-compact-actions .action-btn {
opacity: 1;
}
// Card tile grid: M = 2 tiles/row, L = 1 tile/row.
// --lum-card-scale inline var is suppressed on mobile (isMobile=true in JS),
// so the CSS custom property here wins for thumbnail sizing.
.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 {
overflow-x: auto;
display: flex;
gap: 0.45rem;
padding: 0.25rem 0 0.5rem;
flex: 1;
min-width: 0;
scrollbar-width: none;
-ms-overflow-style: none;
flex-shrink: 0;
width: 100%;
&::-webkit-scrollbar { display: none; }
}
.lumiere-mobile-zoom {
flex-shrink: 0;
}
.lumiere-mobile-filter-pill {
flex-shrink: 0;
padding: 0.3rem 0.8rem;