feat(mobile): collections sheets in the common sheet style

The collections sheets had a header of their own with a hairline, white
square fields and short buttons. Their kit now draws the icon tile header
every phone sheet has (an icon per sheet), round 42 px fields on the field
surface and 42 px footer buttons; the sheets sit on glass with the soft
scroll edge. New list and add place lose their last own field styles: the
labels are the sheets' eyebrows, the search fields, the category picker and
the status choices are round fields of the same height.
This commit is contained in:
Maurice
2026-11-06 16:32:37 +01:00
parent 69d88ffefd
commit 1b24e3dcef
10 changed files with 63 additions and 44 deletions
+5 -5
View File
@@ -170,21 +170,21 @@
"mobile/screens/atlas/MAtlasCountryPopup.tsx": 11,
"mobile/screens/atlas/MAtlasSearch.tsx": 5,
"mobile/screens/atlas/MAtlasStatsCard.tsx": 3,
"mobile/screens/collections/MCollAddSheet.tsx": 7,
"mobile/screens/collections/MCollAddSheet.tsx": 6,
"mobile/screens/collections/MCollCategoryPicker.tsx": 2,
"mobile/screens/collections/MCollEditSheet.tsx": 5,
"mobile/screens/collections/MCollEditSheet.tsx": 2,
"mobile/screens/collections/MCollLabelsSheet.tsx": 6,
"mobile/screens/collections/MCollLinksEditor.tsx": 2,
"mobile/screens/collections/MCollListPickerSheet.tsx": 3,
"mobile/screens/collections/MCollPlaceRow.tsx": 3,
"mobile/screens/collections/MCollPlaceSheet.tsx": 11,
"mobile/screens/collections/MCollShareSheet.tsx": 14,
"mobile/screens/collections/MCollSheetKit.tsx": 6,
"mobile/screens/collections/MCollSheetKit.tsx": 5,
"mobile/screens/collections/MCollTripPickerSheet.tsx": 4,
"mobile/screens/collections/MCollections.tsx": 21,
"mobile/screens/dashboard/MDashWidgets.tsx": 22,
"mobile/screens/dashboard/MDashboard.tsx": 36,
"mobile/screens/dashboard/MNewTripSheet.tsx": 20,
"mobile/screens/dashboard/MNewTripSheet.tsx": 12,
"mobile/screens/dashboard/MUserMenu.tsx": 7,
"mobile/screens/journey/MJourney.tsx": 10,
"mobile/screens/journey/MJourneyCreateSheet.tsx": 10,
@@ -235,7 +235,7 @@
"mobile/screens/trip/sheets/ImpFileStep.tsx": 12,
"mobile/screens/trip/sheets/ImpListStep.tsx": 3,
"mobile/screens/trip/sheets/MAccommodationSheet.tsx": 10,
"mobile/screens/trip/sheets/MBookingFields.tsx": 6,
"mobile/screens/trip/sheets/MBookingFields.tsx": 5,
"mobile/screens/trip/sheets/MBrowseActionsSheet.tsx": 5,
"mobile/screens/trip/sheets/MCostSheet.tsx": 33,
"mobile/screens/trip/sheets/MDaySheet.tsx": 22,
@@ -1,4 +1,4 @@
import { Check, Loader2, MapPin, Search, X } from 'lucide-react'
import { Check, Loader2, MapPin, Search, X, MapPinPlus } from 'lucide-react'
import type { Category, TranslationFn } from '../../../types'
import { STATUS_ORDER } from '../../../pages/collections/collectionsModel'
import MSheet from '../../components/MSheet'
@@ -32,9 +32,10 @@ export default function MCollAddSheet({ open, collectionId, collectionName, list
} = useAddPlaceToCollection({ variant: 'sheet', open, collectionId, collectionName, lists, t, onClose, onAdded })
return (
<MSheet open={open} onClose={onClose} material="opaque" ariaLabel={t('collections.addPlace')}>
<SheetHeader title={t('collections.addPlace')} onClose={onClose} closeLabel={t('common.close')} />
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] py-[14px]">
<MSheet open={open} onClose={onClose} material="glass" ariaLabel={t('collections.addPlace')}>
<SheetHeader
icon={<MapPinPlus size={19} strokeWidth={1.8} />} title={t('collections.addPlace')} onClose={onClose} closeLabel={t('common.close')} />
<div className="m-scroll-fade min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
{/* List picker — only when opened without a fixed target list */}
{collectionId == null && (
<div className="mb-[14px]">
@@ -65,20 +66,20 @@ export default function MCollAddSheet({ open, collectionId, collectionName, list
</div>
)}
{/* Search — picking a result fills the location */}
<div className="flex items-center gap-2 rounded-[14px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] py-1 ps-[13px] pe-[6px]">
<div className="flex h-[42px] items-center gap-2 rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] pe-1 ps-[14px]">
<Search size={15} strokeWidth={2.2} className="flex-none text-m-muted" />
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); void search() } }}
placeholder={t('collections.addPlaceSearch')}
className="min-w-0 flex-1 bg-transparent py-2 font-[inherit] text-[0.8125rem] text-m-ink outline-none placeholder:text-m-faint"
className="min-w-0 flex-1 bg-transparent font-[inherit] text-[0.8125rem] text-m-ink outline-none placeholder:text-m-faint"
/>
<button
type="button"
onClick={search}
disabled={!query.trim() || searching}
className="flex h-8 flex-none items-center gap-[5px] rounded-[10px] bg-m-act px-[14px] text-[0.75rem] font-bold text-m-actfg disabled:opacity-40"
className="flex h-[34px] flex-none items-center gap-[5px] rounded-full bg-m-act px-[14px] text-[length:calc(12px*var(--fs-scale-body,1))] font-semibold text-m-actfg disabled:opacity-40"
>
{searching ? <Loader2 size={13} className="animate-spin" /> : null}
{t('common.search')}
@@ -129,7 +130,7 @@ export default function MCollAddSheet({ open, collectionId, collectionName, list
type="button"
onClick={() => setStatus(s)}
aria-pressed={on}
className={`flex flex-1 items-center justify-center gap-[5px] rounded-full py-2 text-[0.71875rem] font-bold ${
className={`flex h-[42px] min-w-0 flex-1 items-center justify-center gap-[5px] rounded-full text-[0.71875rem] font-bold ${
on ? 'bg-m-act text-m-actfg' : 'border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] text-m-ink'
}`}
>
@@ -32,10 +32,10 @@ export default function MCollCategoryPicker({ categories, value, onChange, t }:
<button
type="button"
onClick={() => setOpen(v => !v)}
className="flex w-full items-center gap-[9px] rounded-[12px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-[13px] py-[11px] text-start"
className="flex h-[42px] w-full items-center gap-[9px] rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] pe-[14px] ps-[8px] text-start"
>
<span
className="flex h-[26px] w-[26px] flex-none items-center justify-center rounded-lg"
className="flex h-[28px] w-[28px] flex-none items-center justify-center rounded-full"
style={{ background: tint(selectedMeta.color, '1f'), color: selectedMeta.color }}
>
<SelectedIcon size={14} strokeWidth={2.2} />
@@ -1,4 +1,4 @@
import { Image, Loader2, Search, Trash2 } from 'lucide-react'
import { Image, Loader2, Search, Trash2, Bookmark } from 'lucide-react'
import type { Collection } from '@trek/shared'
import type { TranslationFn } from '../../../types'
import { useListEditor } from '../../../components/Collections/useListEditor'
@@ -6,7 +6,7 @@ import { normalizeLinkUrl } from '../../../pages/collections/collectionsModel'
import MSheet from '../../components/MSheet'
import MCollLinksEditor from './MCollLinksEditor'
import { listCoverGradient, SWATCH_COLORS } from './collectionsMobileModel'
import { CancelPill, PrimaryPill, SheetFooter, SheetHeader, TEXTAREA_CLS } from './MCollSheetKit'
import { CancelPill, INPUT_CLS, PrimaryPill, SheetFooter, SheetHeader, TEXTAREA_CLS } from './MCollSheetKit'
// Scrims over the cover photo / Unsplash thumbs — fixed dark overlays in both themes.
const COVER_OVERLAY =
@@ -34,22 +34,24 @@ export default function MCollEditSheet({ target, onClose, onCreated, onRequestDe
coverPreview, coverQuery, setCoverQuery, coverResults, searchingCover, saving, close, pickCover, searchCover, pickUnsplash, save,
} = useListEditor({ target, onClose, onCreated, t, defaultColor: SWATCH_COLORS[0], holdLastTarget: true, normalizeLinkUrl })
const label = 'mb-[5px] font-geist text-[0.6875rem] font-bold text-m-muted'
// The sheets' eyebrow (Eyebrow), uppercase over each field.
const label = 'mb-[5px] font-geist text-[0.625rem] font-bold uppercase tracking-[.09em] text-m-faint'
const canDeleteList = editing != null && editing.is_owner !== false
return (
<MSheet
open={target != null}
onClose={close}
material="opaque"
material="glass"
ariaLabel={editing ? t('collections.editListTitle') : t('collections.newList')}
>
<SheetHeader
icon={<Bookmark size={19} strokeWidth={1.8} />}
title={editing ? t('collections.editListTitle') : t('collections.newList')}
onClose={close}
closeLabel={t('common.close')}
/>
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] py-[14px]">
<div className="m-scroll-fade min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
<div className={label}>{t('collections.coverImage')}</div>
<button
type="button"
@@ -75,13 +77,13 @@ export default function MCollEditSheet({ target, onClose, onCreated, onRequestDe
onChange={e => setCoverQuery(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); void searchCover() } }}
placeholder={t('dashboard.unsplashSearchPlaceholder')}
className="min-w-0 flex-1 rounded-[12px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-3 py-[10px] font-geist text-[0.71875rem] text-m-ink outline-none placeholder:text-m-faint"
className={`${INPUT_CLS} min-w-0 flex-1`}
/>
<button
type="button"
onClick={searchCover}
disabled={searchingCover || (!coverQuery.trim() && !name.trim())}
className="flex flex-none items-center gap-[5px] rounded-[12px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-3 py-[10px] text-[0.71875rem] font-semibold text-m-ink disabled:opacity-40"
className="flex h-[42px] flex-none items-center gap-[5px] rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-4 text-[length:calc(13px*var(--fs-scale-body,1))] font-semibold text-m-ink disabled:opacity-40"
>
{searchingCover ? <Loader2 size={12} className="animate-spin" /> : <Search size={12} strokeWidth={2} />} Unsplash
</button>
@@ -113,7 +115,7 @@ export default function MCollEditSheet({ target, onClose, onCreated, onRequestDe
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('collections.listNamePlaceholder')}
className="w-full box-border rounded-[12px] border-[1.5px] border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-[13px] py-[11px] font-[inherit] text-[0.84375rem] font-semibold text-m-ink outline-none placeholder:text-m-faint"
className={`${INPUT_CLS} font-semibold`}
/>
<div className={`${label} mt-[14px] mb-[6px]`}>{t('collections.listColor')}</div>
@@ -92,8 +92,9 @@ export default function MCollLabelsSheet({
const title = mode === 'assign' ? t('collections.labels.assignN', { count: selectedCount }) : t('collections.labels.manage')
return (
<MSheet open={open} onClose={onClose} material="opaque" ariaLabel={title}>
<SheetHeader title={title} onClose={onClose} closeLabel={t('common.close')} />
<MSheet open={open} onClose={onClose} material="glass" ariaLabel={title}>
<SheetHeader
icon={<Tag size={19} strokeWidth={1.8} />} title={title} onClose={onClose} closeLabel={t('common.close')} />
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-4">
{mode === 'manage' && (
<>
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Loader2, ListPlus } from 'lucide-react'
import type { Collection } from '@trek/shared'
import type { TranslationFn } from '../../../types'
import MSheet from '../../components/MSheet'
@@ -40,8 +40,9 @@ export default function MCollListPickerSheet({ mode, lists, count, onPick, onClo
}
return (
<MSheet open={mode != null} onClose={onClose} material="opaque" ariaLabel={title}>
<SheetHeader title={title} onClose={onClose} closeLabel={t('common.close')} />
<MSheet open={mode != null} onClose={onClose} material="glass" ariaLabel={title}>
<SheetHeader
icon={<ListPlus size={19} strokeWidth={1.8} />} title={title} onClose={onClose} closeLabel={t('common.close')} />
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
{lists.length === 0 ? (
<div className="py-8 text-center font-geist text-[0.78125rem] text-m-faint">{t('collections.noOtherLists')}</div>
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Check, ChevronDown, Clock, Crown, Loader2, LogOut, UserPlus, X } from 'lucide-react'
import { Check, ChevronDown, Clock, Crown, Loader2, LogOut, UserPlus, X, Share2 } from 'lucide-react'
import type { CollectionMember } from '@trek/shared'
import { COLLECTION_ROLES } from '@trek/shared'
import type { TranslationFn } from '../../../types'
@@ -54,8 +54,9 @@ export default function MCollShareSheet({
const badge = 'inline-flex flex-none items-center gap-[3px] font-geist text-[0.625rem] font-extrabold'
return (
<MSheet open={open} onClose={onClose} material="opaque" ariaLabel={t('collections.share.title')}>
<SheetHeader title={t('collections.share.titleNamed', { name: collectionName })} onClose={onClose} closeLabel={t('common.close')} />
<MSheet open={open} onClose={onClose} material="glass" ariaLabel={t('collections.share.title')}>
<SheetHeader
icon={<Share2 size={19} strokeWidth={1.8} />} title={t('collections.share.titleNamed', { name: collectionName })} onClose={onClose} closeLabel={t('common.close')} />
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
<div className="mb-2 flex items-center gap-[7px]">
<Eyebrow>{t('collections.share.members').toUpperCase()}</Eyebrow>
@@ -4,22 +4,33 @@ import { X } from 'lucide-react'
/** Shared sheet scaffolding of the collections sheets (opaque floating cards). */
export const INPUT_CLS =
'w-full box-border rounded-[12px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-[13px] py-[11px] font-[inherit] text-[0.8125rem] text-m-ink outline-none placeholder:text-m-faint'
'w-full box-border rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[14px] py-[10px] font-[inherit] text-[0.8125rem] text-m-ink outline-none placeholder:text-m-faint'
export const TEXTAREA_CLS =
'w-full box-border resize-none rounded-[12px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-[13px] py-[11px] font-geist text-[0.78125rem] leading-[1.5] text-m-ink outline-none placeholder:text-m-faint'
'w-full box-border resize-none rounded-[18px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[14px] py-[10px] font-geist text-[0.78125rem] leading-[1.5] text-m-ink outline-none placeholder:text-m-faint'
interface SheetHeaderProps {
title: ReactNode
/** The icon tile in front of the title, as every phone sheet has. */
icon?: ReactNode
sub?: ReactNode
onClose: () => void
closeLabel: string
}
/** Sheet header: 17px/700 title + 34px round close, hairline below. */
export function SheetHeader({ title, onClose, closeLabel }: SheetHeaderProps) {
/** Sheet header: the icon tile, the 17px/700 title (with an optional line under it) and the 34px round close, as TileHeader. */
export function SheetHeader({ title, icon, sub, onClose, closeLabel }: SheetHeaderProps) {
return (
<div className="flex flex-none items-center gap-[11px] border-b border-[color:var(--m-rowbr)] px-[18px] pb-[10px] pt-4">
<div className="min-w-0 flex-1 truncate text-[1.0625rem] font-bold text-m-ink">{title}</div>
<div className="flex flex-none items-center gap-3 px-[18px] pb-1 pt-4">
{icon && (
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-[13px] bg-[color:var(--m-ic)] text-m-ink">
{icon}
</div>
)}
<div className="min-w-0 flex-1">
<div className="truncate text-[1.0625rem] font-bold leading-tight text-m-ink">{title}</div>
{sub && <div className="truncate font-geist text-[0.71875rem] text-m-muted">{sub}</div>}
</div>
<button
type="button"
onClick={onClose}
@@ -55,7 +66,7 @@ export function CancelPill({ onClick, disabled, className = '', children }: Pill
type="button"
onClick={onClick}
disabled={disabled}
className={`rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-4 py-[9px] text-[0.78125rem] font-semibold text-m-ink disabled:opacity-40 ${className}`}
className={`flex h-[42px] items-center justify-center rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-4 text-[length:calc(13px*var(--fs-scale-body,1))] font-semibold text-m-ink disabled:opacity-40 ${className}`}
>
{children}
</button>
@@ -69,7 +80,7 @@ export function PrimaryPill({ onClick, disabled, className = '', children }: Pil
type="button"
onClick={onClick}
disabled={disabled}
className={`flex items-center gap-[6px] rounded-full bg-m-act px-[18px] py-[9px] text-[0.78125rem] font-bold text-m-actfg disabled:opacity-40 ${className}`}
className={`flex h-[42px] items-center justify-center gap-[6px] rounded-full bg-m-act px-4 text-[length:calc(13px*var(--fs-scale-body,1))] font-semibold text-m-actfg disabled:opacity-40 ${className}`}
>
{children}
</button>
@@ -211,8 +211,9 @@ function MCollTourSheet({
const tour = takeOver.tour;
const title = tour?.name ?? t('tours.collection.add');
return (
<MSheet open={takeOver.open} onClose={takeOver.close} material="opaque" ariaLabel={title}>
<SheetHeader title={title} onClose={takeOver.close} closeLabel={t('common.close')} />
<MSheet open={takeOver.open} onClose={takeOver.close} material="glass" ariaLabel={title}>
<SheetHeader
icon={<Route size={19} strokeWidth={1.8} />} title={title} onClose={takeOver.close} closeLabel={t('common.close')} />
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
{tour && (
<p className="mb-3 mt-0 font-geist text-m-muted" style={fs(11)}>
@@ -1,4 +1,4 @@
import { CalendarDays, Copy, Loader2, MapPin, Search } from 'lucide-react'
import { CalendarDays, Copy, Loader2, MapPin, Search, Luggage } from 'lucide-react'
import type { TranslationFn } from '../../../types'
import { useTripCopyPicker } from '../../../components/Collections/useTripCopyPicker'
import MSheet from '../../components/MSheet'
@@ -23,8 +23,9 @@ export default function MCollTripPickerSheet({ open, count, onCopy, onClose, t }
const title = t('collections.copyN', { count })
return (
<MSheet open={open} onClose={onClose} material="opaque" ariaLabel={title}>
<SheetHeader title={title} onClose={onClose} closeLabel={t('common.close')} />
<MSheet open={open} onClose={onClose} material="glass" ariaLabel={title}>
<SheetHeader
icon={<Luggage size={19} strokeWidth={1.8} />} title={title} onClose={onClose} closeLabel={t('common.close')} />
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
<div className="mb-2 flex h-9 items-center gap-2 rounded-[12px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-[13px]">
<Search size={15} strokeWidth={2.2} className="flex-none text-m-muted" />