From ec87f280c327ba41a30e8ebfd39801c079a7dcea Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 6 Nov 2026 16:07:57 +0100 Subject: [PATCH] feat(mobile): journey settings in the trip settings' style The journey settings sheet looked like a different app next to the trip settings. It now uses the same parts: the glass card with the icon tile header and the journey's name under the title, sections as inner cards with a title, count badge and hint, 42 px round fields and buttons, the shared link field, and switch rows for tracks, entry fields and the public link. Those parts (SheetSection and the SHEET_* button and field classes) moved into MTripSheetUi for every sheet to use. Fields and buttons sitting directly in a column no longer shrink below their height; name, subtitle and the archive button were squeezed flat. --- client/scripts/theme-baseline.json | 2 +- .../screens/journey/MJourneySettingsSheet.tsx | 467 +++++++++--------- .../trip/sheets/MTripSettingsSheet.tsx | 56 +-- .../screens/trip/sheets/MTripSheetUi.tsx | 52 ++ 4 files changed, 296 insertions(+), 281 deletions(-) diff --git a/client/scripts/theme-baseline.json b/client/scripts/theme-baseline.json index 3e7cb2314..0dd0fddee 100644 --- a/client/scripts/theme-baseline.json +++ b/client/scripts/theme-baseline.json @@ -190,7 +190,7 @@ "mobile/screens/journey/MJourneyCreateSheet.tsx": 10, "mobile/screens/journey/MJourneyDetail.tsx": 6, "mobile/screens/journey/MJourneyEntrySheet.tsx": 52, - "mobile/screens/journey/MJourneySettingsSheet.tsx": 29, + "mobile/screens/journey/MJourneySettingsSheet.tsx": 2, "mobile/screens/journey/mobileJourneyMeta.ts": 4, "mobile/screens/notifications/MNotificationRow.tsx": 4, "mobile/screens/notifications/MNotifications.tsx": 3, diff --git a/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx b/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx index 4d80a9cb0..0fe218174 100644 --- a/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx +++ b/client/src/mobile/screens/journey/MJourneySettingsSheet.tsx @@ -1,8 +1,17 @@ -import { useRef, useState } from 'react' -import { X, Image, Plus, Trash2, UserPlus, Link as LinkIcon, List, Grid3x3, MapPin, Route, Archive, ArchiveRestore, Undo2, Link2, ListChecks, RefreshCw } from 'lucide-react' +import { useRef, useState, type ReactNode } from 'react' +import { Image, Plus, Trash2, UserPlus, UserMinus, Unlink, Settings2, Link as LinkIcon, List, Grid3x3, MapPin, Route, Archive, ArchiveRestore, Undo2, Link2, ListChecks, RefreshCw } from 'lucide-react' import MLinkField from '../../components/MLinkField' import MSheet from '../../components/MSheet' -import MIconBtn from '../../components/MIconBtn' +import { + Eyebrow, + SHEET_FIELD, + SHEET_PILL, + SHEET_PRIMARY, + SHEET_ROUND, + SHEET_SECONDARY, + SheetSection, + TileHeader, +} from '../trip/sheets/MTripSheetUi' import MToggle from '../../components/MToggle' import ConfirmDialog from '../../../components/shared/ConfirmDialog' import { useTranslation } from '../../../i18n' @@ -58,266 +67,239 @@ export default function MJourneySettingsSheet({ if (availableTrips.length === 0) await loadAvailableTrips() } - const eyebrow = 'font-geist text-[0.625rem] font-bold uppercase tracking-[.09em] text-m-faint' - const inputShell = 'w-full rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[13px] py-[11px] text-m-ink outline-none placeholder:text-m-faint' - const dashedBtn = 'flex w-full items-center justify-center gap-[6px] rounded-xl border border-dashed border-[color:var(--m-rowbr)] p-[11px] text-[0.75rem] font-semibold text-m-muted' - const solidBtn = 'flex w-full items-center justify-center gap-[6px] rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] p-[11px] text-[0.75rem] font-semibold' const linkedTripIds = journey.trips.map(tr => tr.trip_id) const unlinkedTrips = availableTrips.filter(tr => !linkedTripIds.includes(tr.id)) return ( - -
- {t('journey.settings.title')} - - - + +
+ } + title={t('journey.settings.title')} + sub={journey.title} + onClose={onClose} + closeLabel={t('common.close')} + />
-
- {/* Cover */} -
{t('journey.settings.coverImage')}
- -
- {coverSrc ? ( - - ) : ( -
- )} - -
+
+ + +
+ {coverSrc ? ( + + ) : ( +
+ )} + +
+ - {/* Name + subtitle */} -
{t('journey.settings.name')}
- setTitle(e.target.value)} className={`${inputShell} text-[0.84375rem] font-semibold`} /> -
{t('journey.settings.subtitle')}
- setSubtitle(e.target.value)} - placeholder={t('journey.settings.subtitlePlaceholder')} - className={`${inputShell} text-[0.8125rem] font-medium`} - /> + + setTitle(e.target.value)} aria-label={t('journey.settings.name')} className={`${SHEET_FIELD} w-full flex-none font-semibold`} /> + {t('journey.settings.subtitle')} + setSubtitle(e.target.value)} + placeholder={t('journey.settings.subtitlePlaceholder')} + aria-label={t('journey.settings.subtitle')} + className={`${SHEET_FIELD} w-full flex-none`} + /> + {/* Trip GPX tracks on the journey map (#2194) */} -
{t('journey.settings.tracks')}
-
- -
-
{t('journey.settings.showTripTracks')}
-
{t('journey.settings.showTripTracksHint')}
-
- + } + label={t('journey.settings.showTripTracks')} + hint={t('journey.settings.showTripTracksHint')} checked={!!journey.show_trip_tracks} onChange={handleTracksToggle} disabled={savingTracks} - ariaLabel={t('journey.settings.showTripTracks')} /> -
+ + {recordingPrep && ( - <> -
{t('journey.settings.recordings')}
+ - + )} {/* The three fields a journey may put away (discussion #2299) */} -
{t('journey.settings.entryFields')}
-
{t('journey.settings.entryFieldsHint')}
- {([ - ['show_verdict', t('journey.settings.showVerdict')], - ['show_mood', t('journey.settings.showMood')], - ['show_weather', t('journey.settings.showWeather')], - ] as const).map(([field, label]) => ( -
-
{label}
- + {([ + ['show_verdict', t('journey.settings.showVerdict')], + ['show_mood', t('journey.settings.showMood')], + ['show_weather', t('journey.settings.showWeather')], + ] as const).map(([field, label]) => ( + handleFieldToggle(field, next)} disabled={savingField !== null} - ariaLabel={label} /> -
- ))} - - {/* Only when there is something to bring back */} - {onRestoreSuggestions && (journey.dismissed_count ?? 0) > 0 && ( - - )} - - {/* Synced trips */} -
{t('journey.detail.syncedTrips')}
- {journey.trips.map(trip => ( -
- -
-
{trip.title}
-
- {t('mobileJourney.placesCount', { count: trip.place_count ?? 0 })} -
-
- -
- ))} - {journey.trips.length === 0 && ( -

{t('journey.trips.noTripsLinkedSettings')}

- )} - - {addTripOpen && ( -
- {unlinkedTrips.length === 0 && ( -

{t('journey.trips.noTripsAvailable')}

- )} - {unlinkedTrips.map(trip => ( -
- - {trip.title} - -
- ))} -
- )} + )} + - {/* Contributors */} -
{t('journey.detail.contributors')}
- {journey.contributors.map(c => ( -
- - {(c.username || '?')[0].toUpperCase()} - - {c.username} - - {c.role} - - {c.role !== 'owner' && ( + + {journey.trips.map(trip => ( +
+ +
+
{trip.title}
+
+ {t('mobileJourney.placesCount', { count: trip.place_count ?? 0 })} +
+
- )} -
- ))} - - - - - {/* Public share */} -
{t('journey.share.publicShare')}
- {!shareLink ? ( -
+ ))} + {journey.trips.length === 0 && ( +

{t('journey.trips.noTripsLinkedSettings')}

+ )} + - ) : ( - <> - {shareUrl && ( - - )} - {([ - { key: 'share_timeline' as const, label: t('journey.share.timeline'), icon: List }, - { key: 'share_gallery' as const, label: t('journey.share.gallery'), icon: Grid3x3 }, - { key: 'share_map' as const, label: t('journey.share.map'), icon: MapPin }, - ]).map(({ key, label, icon: Icon }) => ( -
- - {label} - toggleSharePerm(key)} ariaLabel={label} /> -
- ))} - - - - )} + {addTripOpen && ( +
+ {unlinkedTrips.length === 0 && ( +

{t('journey.trips.noTripsAvailable')}

+ )} + {unlinkedTrips.map(trip => ( +
+ + {trip.title} + +
+ ))} +
+ )} + - {/* Archive */} - + )} +
+ ))} + +
+ + + + + {!shareLink ? ( + + ) : ( + <> + {shareUrl && ( + + )} + {([ + { key: 'share_timeline' as const, label: t('journey.share.timeline'), icon: List }, + { key: 'share_gallery' as const, label: t('journey.share.gallery'), icon: Grid3x3 }, + { key: 'share_map' as const, label: t('journey.share.map'), icon: MapPin }, + ]).map(({ key, label, icon: Icon }) => ( + } + label={label} + checked={shareLink[key]} + onChange={() => toggleSharePerm(key)} + /> + ))} + + + + )} + + +
-
+
- -
@@ -346,26 +328,45 @@ export default function MJourneySettingsSheet({ } /** Who a trip's linked journal lets in (#2683): shown under the contributors, but changed in that trip. */ -function MTripAccessList({ access, eyebrow }: { access: JourneyTripAccess[] | undefined; eyebrow: string }) { +function MTripAccessList({ access }: { access: JourneyTripAccess[] | undefined }) { const { t } = useTranslation() if (!access?.length) return null return ( - <> -
{t('journey.contributors.viaTrips')}
-

{t('journey.contributors.viaTripsHint')}

+ {access.map(a => ( -
- +
+ + + - {a.username} - {t('journey.contributors.viaTrip', { trip: a.trip_title })} - - - {a.role} + {a.username} + {t('journey.contributors.viaTrip', { trip: a.trip_title })} + {a.role}
))} - + + ) +} + +/** A switch with its label, an optional icon in front and an optional line under the label. */ +function ToggleRow({ icon, label, hint, checked, onChange, disabled }: { + icon?: ReactNode + label: string + hint?: string + checked: boolean + onChange: (next: boolean) => void + disabled?: boolean +}) { + return ( +
+ {icon && {icon}} +
+
{label}
+ {hint &&
{hint}
} +
+ +
) } diff --git a/client/src/mobile/screens/trip/sheets/MTripSettingsSheet.tsx b/client/src/mobile/screens/trip/sheets/MTripSettingsSheet.tsx index 98c39b17f..59ba8b556 100644 --- a/client/src/mobile/screens/trip/sheets/MTripSettingsSheet.tsx +++ b/client/src/mobile/screens/trip/sheets/MTripSettingsSheet.tsx @@ -29,7 +29,15 @@ import MChip from '../../../components/MChip'; import MLinkField from '../../../components/MLinkField'; import MSheet from '../../../components/MSheet'; import MConfirmSheet from '../../settings/MConfirmSheet'; -import { Eyebrow, INNER_CLS, TileHeader } from './MTripSheetUi'; +import { + SHEET_FIELD as FIELD, + SHEET_PILL as PILL, + SHEET_PRIMARY as PRIMARY, + SHEET_ROUND as ROUND, + SHEET_ROUND_PRIMARY as ROUND_PRIMARY, + SheetSection as Section, + TileHeader, +} from './MTripSheetUi'; interface MTripSettingsSheetProps { open: boolean; @@ -44,52 +52,6 @@ type Question = | { kind: 'remove'; id: number; self: boolean } | { kind: 'guest'; id: number }; -const FIELD = - 'h-[42px] min-w-0 flex-1 rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[14px] text-[length:calc(13px*var(--fs-scale-body,1))] font-medium text-m-ink outline-none placeholder:text-m-faint'; -const PRIMARY = - 'flex h-[42px] flex-none items-center gap-1.5 rounded-full bg-m-act px-4 text-[length:calc(13px*var(--fs-scale-body,1))] font-semibold text-m-actfg disabled:opacity-40'; -/** The round icon buttons next to a link: copy, share. */ -/** The round, filled icon button next to a picker or field: invite, add. */ -const ROUND_PRIMARY = - 'flex h-[42px] w-[42px] flex-none items-center justify-center rounded-full bg-m-act text-m-actfg disabled:opacity-40'; -const ROUND = - 'flex h-[42px] w-[42px] flex-none items-center justify-center rounded-full bg-[color:var(--m-ic)] text-m-muted disabled:opacity-40'; -const PILL = - 'flex h-[22px] flex-none items-center gap-[3px] rounded-full px-[8px] font-geist text-[length:calc(9.5px*var(--fs-scale-caption,1))] font-bold uppercase tracking-[.06em]'; - -/** A titled block of the sheet: the eyebrow, an optional line under it and the content on the inner surface. */ -function Section({ - title, - count, - hint, - children, -}: { - title: string; - /** How many there are, as a small badge next to the title rather than glued onto it. */ - count?: number; - hint?: string; - children: ReactNode; -}) { - return ( -
-
- {title} - {count != null && ( - - {count} - - )} -
- {hint && ( -

- {hint} -

- )} -
{children}
-
- ); -} - /** A person of the trip: the avatar, the name with its badges, and the round actions. */ function PersonRow({ name, diff --git a/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx b/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx index d74a9d9d5..01f4f83f8 100644 --- a/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx +++ b/client/src/mobile/screens/trip/sheets/MTripSheetUi.tsx @@ -17,6 +17,58 @@ export function Eyebrow({ children, className = '' }: { children: ReactNode; cla ) } +/** A text field of the sheets: 42 px, round, on the icon surface. */ +export const SHEET_FIELD = + 'h-[42px] min-w-0 flex-1 rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[14px] text-[length:calc(13px*var(--fs-scale-body,1))] font-medium text-m-ink outline-none placeholder:text-m-faint' +/** The filled action with a label. Never icon-only: its padding leaves an icon no room in a circle, use SHEET_ROUND_PRIMARY. */ +export const SHEET_PRIMARY = + 'flex h-[42px] flex-none items-center gap-1.5 rounded-full bg-m-act px-4 text-[length:calc(13px*var(--fs-scale-body,1))] font-semibold text-m-actfg disabled:opacity-40' +/** The round, filled icon button next to a picker or field: invite, add. */ +export const SHEET_ROUND_PRIMARY = + 'flex h-[42px] w-[42px] flex-none items-center justify-center rounded-full bg-m-act text-m-actfg disabled:opacity-40' +/** The quiet action with a label: add, invite, end. Full width in a section. */ +export const SHEET_SECONDARY = + 'flex h-[42px] items-center justify-center gap-1.5 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' +/** The quiet round icon action of a row: remove, rename, make owner. */ +export const SHEET_ROUND = + 'flex h-[42px] w-[42px] flex-none items-center justify-center rounded-full bg-[color:var(--m-ic)] text-m-muted disabled:opacity-40' +/** A small badge inside a row (owner, you). */ +export const SHEET_PILL = + 'flex h-[22px] flex-none items-center gap-[3px] rounded-full px-[8px] font-geist text-[length:calc(9.5px*var(--fs-scale-caption,1))] font-bold uppercase tracking-[.06em]' + +/** A titled block of the sheet: the eyebrow, an optional line under it and the content on the inner surface. */ +export function SheetSection({ + title, + count, + hint, + children, +}: { + title: string + /** How many there are, as a small badge next to the title rather than glued onto it. */ + count?: number + hint?: string + children: ReactNode +}) { + return ( +
+
+ {title} + {count != null && ( + + {count} + + )} +
+ {hint && ( +

+ {hint} +

+ )} +
{children}
+
+ ) +} + interface TileHeaderProps { icon: ReactNode title: ReactNode