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 {