From a004f2168858143ff6304e8b688cc010c43aaefc Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 18 Jun 2026 08:22:27 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20mobile=20ergonomics=20on=20Lumi=C3=A8re?= =?UTF-8?q?=20project=20list=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compact row (xs): wrap format/owner/date in a lumiere-compact-meta div that uses display:contents on desktop (preserving the 6-column grid) and switches to a 2-row grid layout on mobile — row 1: checkbox/name/actions, row 2: format·owner·date. Actions are always visible on touch devices. Filter access: add a horizontal scrollable filter-pill bar (d-md-none) to the Lumière header so users can switch between All/Yours/Shared/Archived/ Trashed on phone without needing the desktop sidebar. Also: hide zoom control on mobile (d-md-none), auto-force compact view on mobile via useIsMobile hook, fix search form min-width overflow on xs. Co-Authored-By: Claude Sonnet 4.6 --- .../components/project-list-lumiere.tsx | 81 +++++++++++--- .../pages/project-list-lumiere.scss | 102 +++++++++++++++++- 2 files changed, 168 insertions(+), 15 deletions(-) diff --git a/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx b/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx index ccb44c0fba..c11f654302 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx @@ -1,6 +1,15 @@ -import React, { memo, useCallback, useEffect, useRef, useState } from 'react' +import React, { + memo, + useCallback, + useEffect, + useRef, + useState, +} from 'react' import { useTranslation } from 'react-i18next' -import { useProjectListContext } from '../context/project-list-context' +import { + Filter, + useProjectListContext, +} from '../context/project-list-context' import { Project } from '../../../../../types/project/dashboard/api' import { getOwnerName } from '../util/project' import { fromNowDate } from '../../../utils/dates' @@ -35,6 +44,21 @@ import ActionsCell from './table/cells/actions-cell' import InlineTags from './table/cells/inline-tags' import WelcomePageContent from './welcome-page-content' +// ── Mobile breakpoint ───────────────────────────────────────────────────────── + +function useIsMobile() { + const [mobile, setMobile] = useState( + () => window.matchMedia('(max-width: 767px)').matches + ) + useEffect(() => { + const mq = window.matchMedia('(max-width: 767px)') + const handler = (e: MediaQueryListEvent) => setMobile(e.matches) + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + }, []) + return mobile +} + // ── Tile zoom ───────────────────────────────────────────────────────────────── type ZoomLevel = 0 | 1 | 1.35 | 1.75 @@ -209,14 +233,16 @@ const ProjectCardCompact = memo(function ProjectCardCompact({ -
- -
-
- -
-
- +
+
+ +
+
+ +
+
+ +
@@ -230,6 +256,7 @@ export function ProjectListLumiere() { const footerProps = getMeta('ol-footer') const { t } = useTranslation() const [cardScale, setCardScale] = useLumiereCardScale() + const isMobile = useIsMobile() const { error, visibleProjects, @@ -241,13 +268,25 @@ export function ProjectListLumiere() { selectedTagId, selectedProjects, selectOrUnselectAllProjects, + selectFilter, } = useProjectListContext() + // On mobile, always use the compact (XS) row view — cards overflow on small screens + const effectiveScale = isMobile ? 0 : cardScale + const selectedTag = tags.find(tag => tag._id === selectedTagId) const allSelected = visibleProjects.length > 0 && selectedProjects.length === visibleProjects.length + const MOBILE_FILTERS: { f: Filter; label: string }[] = [ + { f: 'all', label: t('all_projects') }, + { f: 'owned', label: t('your_projects') }, + { f: 'shared', label: t('shared_with_you') }, + { f: 'archived', label: t('archived_projects') }, + { f: 'trashed', label: t('trashed_projects') }, + ] + const checkAllRef = useRef(null) useEffect(() => { if (checkAllRef.current) { @@ -295,7 +334,7 @@ export function ProjectListLumiere() { id="lumiere-title" />
@@ -312,6 +351,20 @@ export function ProjectListLumiere() { ))}
+ {/* Mobile-only filter pills replacing the hidden sidebar */} +
+ {MOBILE_FILTERS.map(({ f, label }) => ( + + ))} +
{t('no_projects')}

) : (
{visibleProjects.map(project => - cardScale === 0 ? ( + effectiveScale === 0 ? ( ) : ( diff --git a/services/web/frontend/stylesheets/pages/project-list-lumiere.scss b/services/web/frontend/stylesheets/pages/project-list-lumiere.scss index e3695f109a..356c03c0fe 100644 --- a/services/web/frontend/stylesheets/pages/project-list-lumiere.scss +++ b/services/web/frontend/stylesheets/pages/project-list-lumiere.scss @@ -341,9 +341,13 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi flex-wrap: wrap; } - // Search bar — wide enough to show the full placeholder + // Search bar — wide enough to show the full placeholder (relaxed on mobile) form.project-search .form-control { min-width: 360px; + + @media (max-width: 767px) { + min-width: 0; + } } .lumiere-title { @@ -836,6 +840,12 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi text-overflow: ellipsis; } + // On desktop, the meta wrapper is invisible to grid layout — its children + // participate as direct grid items, preserving the 6-column alignment. + .lumiere-compact-meta { + display: contents; + } + .lumiere-compact-actions { display: flex; align-items: center; @@ -860,6 +870,96 @@ $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] + + @media (max-width: 767px) { + .lumiere-compact-row { + grid-template-columns: 28px 1fr auto; + grid-template-rows: auto auto; + row-gap: 0.2rem; + + .lumiere-compact-checkbox { + grid-column: 1; + grid-row: 1 / 3; + align-self: center; + } + + .lumiere-compact-name-cell { + grid-column: 2; + grid-row: 1; + } + + .lumiere-compact-actions { + grid-column: 3; + grid-row: 1; + align-self: center; + } + + // Meta wrapper becomes a flex row spanning the second line + .lumiere-compact-meta { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; + 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; + } + } + } + } + + // ── Mobile filter pills (replaces hidden sidebar on xs/sm) ──────────────── + + .lumiere-mobile-filters { + overflow-x: auto; + gap: 0.45rem; + padding: 0.25rem 0 0.5rem; + scrollbar-width: none; + -ms-overflow-style: none; + flex-shrink: 0; + width: 100%; + + &::-webkit-scrollbar { display: none; } + } + + .lumiere-mobile-filter-pill { + flex-shrink: 0; + padding: 0.3rem 0.8rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 500; + border: 1.5px solid $lum-border; + background: rgba(255, 255, 255, 0.7); + color: $lum-text-sub; + cursor: pointer; + white-space: nowrap; + transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease; + line-height: 1.4; + + &.active { + background: $lum-teal; + border-color: $lum-teal; + color: #fff; + font-weight: 600; + } + + &:hover:not(.active), + &:focus:not(.active) { + border-color: rgba($lum-teal, 0.55); + color: $lum-teal; + background: rgba($lum-teal, 0.07); + } + } + // ── Selection bar — tool buttons ────────────────────────────────────────── .lumiere-selection-bar {