lumiere: fix mobile overflow + restore tile view with XS/M/L zoom
Build and Deploy Verso / deploy (push) Successful in 1m42s
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:
+65
-19
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user