feat(mobile): confirm dialogs and Vacay sheets in the common sheet style

The shared confirm sheet (44 places) gets the icon tile with the title, a
warning icon for destructive questions, and the sheets' 42 px buttons with
the danger colour on a destructive confirm. The dashboard's and the
collections' own confirm dialogs now use it.

Vacay invite, shared calendars and settings get the icon tile header; the
settings' rows separated by hairlines become cards on the inner surface, as
the trip settings' sections.
This commit is contained in:
Maurice
2026-11-06 16:40:06 +01:00
parent 45361c276c
commit ffd848e5dc
8 changed files with 108 additions and 103 deletions
+5 -5
View File
@@ -181,9 +181,9 @@
"mobile/screens/collections/MCollShareSheet.tsx": 14,
"mobile/screens/collections/MCollSheetKit.tsx": 5,
"mobile/screens/collections/MCollTripPickerSheet.tsx": 4,
"mobile/screens/collections/MCollections.tsx": 21,
"mobile/screens/collections/MCollections.tsx": 18,
"mobile/screens/dashboard/MDashWidgets.tsx": 22,
"mobile/screens/dashboard/MDashboard.tsx": 36,
"mobile/screens/dashboard/MDashboard.tsx": 31,
"mobile/screens/dashboard/MNewTripSheet.tsx": 12,
"mobile/screens/dashboard/MUserMenu.tsx": 7,
"mobile/screens/journey/MJourney.tsx": 10,
@@ -279,10 +279,10 @@
"mobile/screens/trip/tabs/filesModel.ts": 5,
"mobile/screens/trip/tabs/tabChrome.tsx": 12,
"mobile/screens/vacay/MVacay.tsx": 17,
"mobile/screens/vacay/MVacayInviteSheet.tsx": 9,
"mobile/screens/vacay/MVacayInviteSheet.tsx": 8,
"mobile/screens/vacay/MVacayMonth.tsx": 3,
"mobile/screens/vacay/MVacaySettingsSheet.tsx": 35,
"mobile/screens/vacay/MVacayShareSheet.tsx": 11,
"mobile/screens/vacay/MVacaySettingsSheet.tsx": 34,
"mobile/screens/vacay/MVacayShareSheet.tsx": 10,
"mobile/screens/vacay/vacayDayModel.ts": 1,
"pages/AtlasPage.tsx": 32,
"pages/DashboardPage.tsx": 1,
@@ -12,7 +12,6 @@ import { STATUS_ORDER } from '../../../pages/collections/collectionsModel'
import type { StatusFilter } from '../../../store/collectionStore'
import { ALL_SAVED } from '../../../store/collectionStore'
import CollectionMap from '../../../components/Collections/CollectionMap'
import MSheet from '../../components/MSheet'
import MCollPlaceRow from './MCollPlaceRow'
import MCollPlaceSheet from './MCollPlaceSheet'
import MCollAddSheet from './MCollAddSheet'
@@ -24,7 +23,7 @@ import MCollListPickerSheet from './MCollListPickerSheet'
import MCollTours from './MCollTours'
import MCollInvites from './MCollInvites'
import { STATUS_SPEC } from './collectionsMobileModel'
import { CancelPill, PrimaryPill } from './MCollSheetKit'
import MConfirmSheet from '../settings/MConfirmSheet'
const HEADER_CIRCLE =
'flex h-[38px] w-[38px] flex-none items-center justify-center rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-sheet)] text-m-ink shadow-[0_5px_12px_-8px_rgba(0,0,0,.18)]'
@@ -546,23 +545,16 @@ export default function MCollections() {
/>
{/* Delete-list confirm */}
<MSheet
<MConfirmSheet
open={c.confirmDeleteList != null}
onClose={() => c.setConfirmDeleteList(null)}
material="opaque"
ariaLabel={t('collections.deleteList')}
>
<div className="px-[18px] pb-[18px] pt-4">
<div className="text-[1.0625rem] font-bold text-m-ink">{t('collections.deleteList')}</div>
<div className="mt-2 font-geist text-[0.78125rem] leading-[1.5] text-m-muted">{t('collections.deleteListConfirm')}</div>
<div className="mt-4 flex items-center gap-2">
<CancelPill className="ms-auto" onClick={() => c.setConfirmDeleteList(null)}>{t('common.cancel')}</CancelPill>
<PrimaryPill onClick={c.handleDeleteList} className="!bg-[color:var(--m-st-danger)] !text-white">
<Trash2 size={14} strokeWidth={2.2} /> {t('common.delete')}
</PrimaryPill>
</div>
</div>
</MSheet>
title={t('collections.deleteList')}
message={t('collections.deleteListConfirm')}
confirmLabel={t('common.delete')}
cancelLabel={t('common.cancel')}
danger
onConfirm={c.handleDeleteList}
/>
</div>
)
}
@@ -21,13 +21,13 @@ import { entityGradient } from '../../../utils/gradients'
import MGlassBar from '../../components/MGlassBar'
import MIconBtn from '../../components/MIconBtn'
import MSegmented from '../../components/MSegmented'
import MSheet from '../../components/MSheet'
import MUserMenu from './MUserMenu'
import { useMobileDashOrder, useMobileDashVisibility, MobileDashWidget } from './MDashWidgets'
import MNewTripSheet from './MNewTripSheet'
import type { MobileDashToken } from '@trek/shared'
import { usePullToRefresh } from '../../usePullToRefresh'
import MPullIndicator from '../../components/MPullIndicator'
import SharedConfirmSheet from '../settings/MConfirmSheet'
type TripFilter = 'planned' | 'archive' | 'completed'
@@ -581,36 +581,11 @@ function ListStat({ value, label }: { value: number; label: string }): React.Rea
)
}
// ── Confirm sheet (delete / copy) ────────────────────────────────────────────
function MConfirmSheet({ open, title, message, confirmLabel, danger = false, onConfirm, onClose }: {
// ── Confirm sheet (delete / copy): the shared one, with the common cancel ─────
function MConfirmSheet(props: {
open: boolean; title: string; message: string; confirmLabel: string
danger?: boolean; onConfirm: () => void; onClose: () => void
}): React.ReactElement {
const { t } = useTranslation()
return (
<MSheet open={open} onClose={onClose} variant="card" ariaLabel={title}>
<div className="p-5">
<div className="text-[1.0625rem] font-bold">{title}</div>
<div className="mt-2 text-[0.8125rem] leading-relaxed text-m-muted">{message}</div>
<div className="mt-5 flex gap-2">
<button
type="button"
onClick={onClose}
className="flex-1 rounded-full bg-[color:var(--m-ic)] py-[10px] text-[0.8125rem] font-semibold text-m-ink"
>
{t('common.cancel')}
</button>
<button
type="button"
onClick={onConfirm}
className={`flex-1 rounded-full py-[10px] text-[0.8125rem] font-semibold ${
danger ? 'bg-[color:var(--m-st-danger)] text-white' : 'bg-m-act text-m-actfg'
}`}
>
{confirmLabel}
</button>
</div>
</div>
</MSheet>
)
return <SharedConfirmSheet {...props} cancelLabel={t('common.cancel')} />
}
@@ -1,6 +1,7 @@
import { ReactNode } from 'react'
import { AlertTriangle, CircleHelp, Info } from 'lucide-react'
import MSheet from '../../components/MSheet'
import { MSetButton } from './MSettingsUi'
import { SHEET_DANGER, SHEET_PRIMARY, SHEET_SECONDARY } from '../trip/sheets/MTripSheetUi'
interface MConfirmSheetProps {
open: boolean
@@ -17,9 +18,11 @@ interface MConfirmSheetProps {
}
/**
* Small confirm dialog as a centred floating card. Without onConfirm it is a
* plain notice. The card stops at the screen height: a long message or list
* scrolls between the title and the buttons, which always stay in reach.
* Small confirm dialog as a centred floating card, in the shape of every phone
* sheet: the icon tile and title, the message, the sheets' 42 px buttons.
* Without onConfirm it is a plain notice. The card stops at the screen height:
* a long message or list scrolls between the title and the buttons, which
* always stay in reach.
*/
export default function MConfirmSheet({
open,
@@ -33,25 +36,42 @@ export default function MConfirmSheet({
onConfirm,
children,
}: MConfirmSheetProps) {
const Icon = danger ? AlertTriangle : onConfirm ? CircleHelp : Info
return (
<MSheet open={open} onClose={onClose} variant="card" material="opaque" ariaLabel={title}>
<div className="flex min-h-0 flex-col p-[18px]">
<div className="flex-none text-[0.9375rem] font-extrabold text-m-ink">{title}</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
<p className="mt-2 text-[0.78125rem] leading-relaxed text-m-muted text-pretty">{message}</p>
{children}
</div>
{/* A long label moves its button to a row of its own instead of wrapping inside it. */}
<div className="mt-4 flex flex-none flex-wrap justify-end gap-2">
<MSetButton variant="ghost" onClick={onClose} className="whitespace-nowrap">
{cancelLabel}
</MSetButton>
{onConfirm && confirmLabel && (
<MSetButton variant={danger ? 'danger' : 'primary'} onClick={onConfirm} disabled={busy} className="whitespace-nowrap">
{confirmLabel}
</MSetButton>
)}
</div>
<MSheet open={open} onClose={onClose} variant="card" material="glass" ariaLabel={title}>
<div className="flex flex-none items-center gap-3 px-[18px] pt-4">
<span
className={`flex h-10 w-10 flex-none items-center justify-center rounded-[13px] bg-[color:var(--m-ic)] ${
danger ? 'text-[color:var(--m-st-danger)]' : 'text-m-ink'
}`}
>
<Icon size={19} strokeWidth={1.8} />
</span>
<div className="min-w-0 flex-1 text-[1.0625rem] font-bold leading-tight text-m-ink text-pretty">{title}</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-[18px] pt-3">
<p className="text-[0.8125rem] leading-relaxed text-m-muted text-pretty">{message}</p>
{children}
</div>
{/* A long label moves its button to a row of its own instead of wrapping inside it. */}
<div className="flex flex-none flex-wrap justify-end gap-2 px-[18px] pb-[18px] pt-4">
<button type="button" onClick={onClose} className={`${SHEET_SECONDARY} whitespace-nowrap`}>
{cancelLabel}
</button>
{onConfirm && confirmLabel && (
<button
type="button"
onClick={onConfirm}
disabled={busy}
className={
danger
? `${SHEET_DANGER} whitespace-nowrap`
: `${SHEET_PRIMARY} whitespace-nowrap`
}
>
{confirmLabel}
</button>
)}
</div>
</MSheet>
)
@@ -29,6 +29,9 @@ export const SHEET_ROUND_PRIMARY =
/** 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'
/** A destructive action with a label: the quiet button's shape, the danger colour on its text. */
export const SHEET_DANGER =
'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-[color:var(--m-st-danger)] 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'
@@ -1,11 +1,11 @@
import { ChevronDown, Clock, Loader2, X } from 'lucide-react'
import { ChevronDown, Clock, Loader2, UserPlus } from 'lucide-react'
import MSheet from '../../components/MSheet'
import MIconBtn from '../../components/MIconBtn'
import MUserPickerList from '../../components/MUserPickerList'
import { useVacayStore } from '../../../store/vacayStore'
import { useTranslation } from '../../../i18n'
import { type VacayUser } from '../../../types'
import { useVacayUserPicker } from '../../../components/Vacay/useVacayUserPicker'
import { TileHeader } from '../trip/sheets/MTripSheetUi'
interface MVacayInviteSheetProps {
open: boolean
@@ -33,12 +33,12 @@ export default function MVacayInviteSheet({ open, onClose }: MVacayInviteSheetPr
return (
<MSheet open={open} onClose={onClose} variant="card" material="glass" ariaLabel={t('vacay.inviteUser')}>
<div className="px-[18px] pb-[18px] pt-4">
<div className="flex items-center gap-3">
<div className="flex-1 text-[1.0625rem] font-bold">{t('vacay.inviteUser')}</div>
<MIconBtn variant="neutral" size={34} onClick={onClose} ariaLabel={t('common.close')}>
<X size={15} strokeWidth={2.2} />
</MIconBtn>
</div>
<TileHeader
icon={<UserPlus size={19} strokeWidth={1.8} />}
title={t('vacay.inviteUser')}
onClose={onClose}
closeLabel={t('common.close')}
/>
<div className="mb-3 mt-2 font-geist text-[0.75rem] leading-normal text-m-muted">
{t('vacay.inviteHint')}
</div>
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { ArrowRightLeft, Building2, Calendar, CalendarRange, CalendarX, ChevronDown, Globe, GraduationCap, Loader2, Plus, Trash2, Unlink, X } from 'lucide-react'
import { ArrowRightLeft, Building2, Calendar, CalendarRange, CalendarX, ChevronDown, Globe, GraduationCap, Loader2, Plus, Trash2, Unlink, X, Settings2 } from 'lucide-react'
import MSheet from '../../components/MSheet'
import MIconBtn from '../../components/MIconBtn'
import MToggle from '../../components/MToggle'
@@ -13,6 +13,7 @@ import {
} from '../../../components/Vacay/useVacaySettings'
import { FALLBACK_PERSON_COLOR } from './vacayDayModel'
import type { VacayHolidayCalendar } from '../../../types'
import { INNER_CLS, TileHeader } from '../trip/sheets/MTripSheetUi'
const CALENDAR_COLORS = ['#fecaca', '#fed7aa', '#fde68a', '#bbf7d0', '#a5f3fc', '#c7d2fe', '#e9d5ff', '#fda4af']
@@ -28,6 +29,9 @@ interface MVacaySettingsSheetProps {
* start, carry-over, company holidays, public-holiday calendars (nager.at —
* label/color/country/region, several) and dissolving the Fusion.
*/
/** Each setting is a card on the inner surface, as the trip settings' sections. */
const CARD = `flex-none rounded-[18px] px-3 pb-1 ${INNER_CLS}`
export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsSheetProps) {
const { t, language } = useTranslation()
const { plan, updatePlan, addHolidayCalendar, updateHolidayCalendar, deleteHolidayCalendar, isFused, users } = useVacayStore()
@@ -52,15 +56,18 @@ export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsShe
]
return (
<MSheet open={open} onClose={onClose} variant="card" material="opaque" ariaLabel={t('vacay.settings')} className="h-[calc(100dvh-120px)]">
<div className="flex flex-none items-center gap-3 border-b border-[color:var(--m-rowbr)] px-[18px] pb-[10px] pt-4">
<div className="flex-1 text-[1.0625rem] font-bold">{t('vacay.settings')}</div>
<MIconBtn variant="neutral" size={34} onClick={onClose} ariaLabel={t('common.close')}>
<X size={15} strokeWidth={2.2} />
</MIconBtn>
<MSheet open={open} onClose={onClose} variant="card" material="glass" ariaLabel={t('vacay.settings')} className="h-[calc(100dvh-120px)]">
<div className="flex-none px-[18px] pt-4">
<TileHeader
icon={<Settings2 size={19} strokeWidth={1.8} />}
title={t('vacay.settings')}
onClose={onClose}
closeLabel={t('common.close')}
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-2">
<div className="m-scroll-fade mt-2 flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">
<section className={CARD}>
{/* Block weekends */}
<div className="flex items-start gap-[11px] py-3">
<CalendarX size={17} strokeWidth={2} className="mt-[1px] flex-none text-m-muted" />
@@ -94,7 +101,8 @@ export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsShe
</div>
</>
)}
<div className="my-2 h-px bg-[color:var(--m-rowbr)]" />
</section>
<section className={CARD}>
{/* Week start */}
<div className="flex items-start gap-[11px] py-3">
@@ -121,7 +129,8 @@ export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsShe
})}
</span>
</div>
<div className="my-2 h-px bg-[color:var(--m-rowbr)]" />
</section>
<section className={CARD}>
{/* Carry over */}
<div className="flex items-start gap-[11px] py-3">
@@ -132,11 +141,13 @@ export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsShe
</div>
<MToggle checked={plan.carry_over_enabled} onChange={() => updatePlan({ carry_over_enabled: !plan.carry_over_enabled })} ariaLabel={t('vacay.carryOver')} />
</div>
<div className="my-2 h-px bg-[color:var(--m-rowbr)]" />
</section>
<section className={CARD}>
{/* Vacation year (#737) — personal, unlike the plan settings around it */}
<YearTypeRows />
<div className="my-2 h-px bg-[color:var(--m-rowbr)]" />
</section>
<section className={CARD}>
{/* Company holidays */}
<div className="flex items-start gap-[11px] py-3">
@@ -147,7 +158,8 @@ export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsShe
</div>
<MToggle checked={plan.company_holidays_enabled} onChange={() => updatePlan({ company_holidays_enabled: !plan.company_holidays_enabled })} ariaLabel={t('vacay.companyHolidays')} />
</div>
<div className="my-2 h-px bg-[color:var(--m-rowbr)]" />
</section>
<section className={CARD}>
{/* Public holidays + calendar editor */}
<div className="flex items-start gap-[11px] py-3">
@@ -189,7 +201,8 @@ export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsShe
)}
</>
)}
<div className="my-2 h-px bg-[color:var(--m-rowbr)]" />
</section>
<section className={CARD}>
{/* School holidays + calendar editor */}
<div className="flex items-start gap-[11px] py-3">
@@ -233,6 +246,8 @@ export default function MVacaySettingsSheet({ open, onClose }: MVacaySettingsShe
</>
)}
</section>
{/* Dissolve Fusion */}
{isFused && (
<div className="mt-[14px] rounded-2xl border border-[rgba(214,39,59,.16)] bg-[rgba(214,39,59,.07)] p-[13px]">
@@ -1,11 +1,11 @@
import { ChevronDown, Eye, EyeOff, Loader2, Share2, X } from 'lucide-react'
import { ChevronDown, Eye, EyeOff, Loader2, Share2 } from 'lucide-react'
import MSheet from '../../components/MSheet'
import MIconBtn from '../../components/MIconBtn'
import MUserPickerList from '../../components/MUserPickerList'
import { useVacayStore } from '../../../store/vacayStore'
import { useTranslation } from '../../../i18n'
import { useVacayShareActions } from '../../../components/Vacay/useVacayShareActions'
import { useVacayUserPicker } from '../../../components/Vacay/useVacayUserPicker'
import { TileHeader } from '../trip/sheets/MTripSheetUi'
interface MVacayShareSheetProps {
open: boolean
@@ -34,12 +34,12 @@ export default function MVacayShareSheet({ open, onClose }: MVacayShareSheetProp
return (
<MSheet open={open} onClose={onClose} variant="card" material="glass" ariaLabel={t('vacay.sharedCalendars')}>
<div className="px-[18px] pb-[18px] pt-4">
<div className="flex items-center gap-3">
<div className="flex-1 text-[1.0625rem] font-bold">{t('vacay.sharedCalendars')}</div>
<MIconBtn variant="neutral" size={34} onClick={onClose} ariaLabel={t('common.close')}>
<X size={15} strokeWidth={2.2} />
</MIconBtn>
</div>
<TileHeader
icon={<Share2 size={19} strokeWidth={1.8} />}
title={t('vacay.sharedCalendars')}
onClose={onClose}
closeLabel={t('common.close')}
/>
<div className="mb-3 mt-2 font-geist text-[0.75rem] leading-normal text-m-muted">
{t('vacay.shareCalendarHint')}
</div>