feat(mobile): admin, settings picker and place sheets in the common style, all sheets on glass

Admin sheets: their frame draws the icon tile header (an icon per sheet, the
plugin sheets' icons moved out of the title into the tile), the soft scroll
edge and 42 px footer buttons; admin fields use the sheets' eyebrow labels
and round inputs. The settings picker, the bulk category sheet and the save
to list sheet get the common header (TileHeader may now leave out the icon
for a plain picker). Every phone sheet now uses the glass material, as the
trip settings do; it stays 94 % opaque, so forms over the map remain
legible.
This commit is contained in:
Maurice
2026-11-06 16:45:09 +01:00
parent ffd848e5dc
commit 27c19b5ff5
24 changed files with 110 additions and 84 deletions
+3 -3
View File
@@ -201,7 +201,7 @@
"mobile/screens/settings/MMobileNavCustomizer.tsx": 8,
"mobile/screens/settings/MPhotoProvidersSection.tsx": 2,
"mobile/screens/settings/MScopeGroupPicker.tsx": 5,
"mobile/screens/settings/MSetPickerSheet.tsx": 2,
"mobile/screens/settings/MSetPickerSheet.tsx": 1,
"mobile/screens/settings/MSettings.tsx": 2,
"mobile/screens/settings/MSettingsAbout.tsx": 5,
"mobile/screens/settings/MSettingsAccount.tsx": 20,
@@ -216,9 +216,9 @@
"mobile/screens/trip/MTripLoadingSplash.tsx": 2,
"mobile/screens/trip/MTripShell.tsx": 9,
"mobile/screens/trip/places/MPlacesBrowser.tsx": 10,
"mobile/screens/trip/places/MPlacesBulkCategorySheet.tsx": 4,
"mobile/screens/trip/places/MPlacesBulkCategorySheet.tsx": 2,
"mobile/screens/trip/places/MPlacesFilterControls.tsx": 1,
"mobile/screens/trip/places/MPlacesSaveToCollectionSheet.tsx": 7,
"mobile/screens/trip/places/MPlacesSaveToCollectionSheet.tsx": 6,
"mobile/screens/trip/places/MPlacesToursModeSwitch.tsx": 1,
"mobile/screens/trip/plan/MPlanTimeline.tsx": 12,
"mobile/screens/trip/plan/MPlanTimelineRows.tsx": 24,
@@ -1,12 +1,7 @@
import { useState } from 'react'
import { useFormatDate } from '../../../hooks/useDateFormat'
import MPluginVersionDate from './MPluginVersionDate'
import {
Blocks, AlertTriangle, PackageOpen, RefreshCw, Trash2, Download, Bug, X, ShieldCheck, UploadCloud,
ArrowUpCircle, Github, ExternalLink, ChevronDown, Check, Lock, Search, Link2, KeyRound, ShieldAlert,
SlidersHorizontal, ArrowUpDown, CircleDot, MoreHorizontal, RotateCw, Database,
Globe, Info, History, PauseCircle, Puzzle, Bot,
} from 'lucide-react'
import { Blocks, AlertTriangle, PackageOpen, RefreshCw, Trash2, Download, Bug, X, ShieldCheck, UploadCloud, ArrowUpCircle, Github, ExternalLink, ChevronDown, Check, Lock, Search, Link2, KeyRound, ShieldAlert, SlidersHorizontal, ArrowUpDown, CircleDot, MoreHorizontal, RotateCw, Database, Globe, Info, History, PauseCircle, Puzzle, Bot, ListChecks } from 'lucide-react'
import PluginIcon from '../../../components/shared/PluginIcon'
import { type RangeWarning } from '../../../components/Admin/useRangeBypass'
import { deriveCaps } from '../../../components/Admin/pluginCaps'
@@ -353,9 +348,10 @@ export default function MAdminPluginsPanel() {
{/* Error-log sheet */}
{errorsFor && (
<MSheet open onClose={() => setErrorsFor(null)} variant="bottom" material="opaque" ariaLabel={t('admin.plugins.errorLog')}>
<MSheet open onClose={() => setErrorsFor(null)} variant="bottom" material="glass" ariaLabel={t('admin.plugins.errorLog')}>
<MAdminSheetFrame
title={<span className="flex items-center gap-2"><Bug size={15} /> {errorsFor.id} — {t('admin.plugins.errorLog')}</span>}
icon={<Bug size={19} strokeWidth={1.8} />}
title={<>{errorsFor.id} — {t('admin.plugins.errorLog')}</>}
onClose={() => setErrorsFor(null)}
>
<div className="font-mono text-[0.6875rem]">
@@ -374,9 +370,10 @@ export default function MAdminPluginsPanel() {
{/* Operator-supplied egress hosts */}
{egressFor && (
<MSheet open onClose={() => setEgressFor(null)} variant="bottom" material="opaque" ariaLabel={t('admin.plugins.allowedHosts')}>
<MSheet open onClose={() => setEgressFor(null)} variant="bottom" material="glass" ariaLabel={t('admin.plugins.allowedHosts')}>
<MAdminSheetFrame
title={<span className="flex items-center gap-2"><Globe size={15} /> {egressFor.id} — {t('admin.plugins.allowedHosts')}</span>}
icon={<Globe size={19} strokeWidth={1.8} />}
title={<>{egressFor.id} — {t('admin.plugins.allowedHosts')}</>}
onClose={() => setEgressFor(null)}
>
{!egressFor.supported ? (
@@ -421,9 +418,10 @@ export default function MAdminPluginsPanel() {
{/* Instance-wide settings (the admin-owned scope:'instance' fields) */}
{settings.form && (
<MSheet open onClose={settings.close} variant="bottom" material="opaque" ariaLabel={t('admin.plugins.instanceSettings')}>
<MSheet open onClose={settings.close} variant="bottom" material="glass" ariaLabel={t('admin.plugins.instanceSettings')}>
<MAdminSheetFrame
title={<span className="flex items-center gap-2"><SlidersHorizontal size={15} /> {settings.form.id} — {t('admin.plugins.instanceSettings')}</span>}
icon={<SlidersHorizontal size={19} strokeWidth={1.8} />}
title={<>{settings.form.id} — {t('admin.plugins.instanceSettings')}</>}
onClose={settings.close}
>
<div className="space-y-4">
@@ -602,8 +600,8 @@ function PickerSheet({ open, onClose, title, options, value, onPick }: {
options: Array<[string, string]>; value: string; onPick: (v: string) => void
}) {
return (
<MSheet open={open} onClose={onClose} variant="bottom" material="opaque" ariaLabel={title}>
<MAdminSheetFrame title={title} onClose={onClose}>
<MSheet open={open} onClose={onClose} variant="bottom" material="glass" ariaLabel={title}>
<MAdminSheetFrame icon={<ListChecks size={19} strokeWidth={1.8} />} title={title} onClose={onClose}>
<div className="space-y-1">
{options.map(([v, lbl]) => (
<button key={v} type="button" onClick={() => { onPick(v); onClose() }}
@@ -760,8 +758,8 @@ function RowActionsSheet({ p, t, onClose, onRestart, onErrors, onEgress, onSetti
const linkable = p.source_repo && p.source_repo !== 'local:upload' && p.source_repo !== 'local:link'
const rowClass = 'flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-start text-[0.8125rem] font-semibold text-m-ink'
return (
<MSheet open onClose={onClose} variant="bottom" material="opaque" ariaLabel={p.name}>
<MAdminSheetFrame title={p.name} onClose={onClose}>
<MSheet open onClose={onClose} variant="bottom" material="glass" ariaLabel={p.name}>
<MAdminSheetFrame icon={<Puzzle size={19} strokeWidth={1.8} />} title={p.name} onClose={onClose}>
<div className="space-y-1">
{p.enabled === 1 && (
<button type="button" className={rowClass} onClick={onRestart}><RotateCw size={16} /> {t('admin.plugins.restart')}</button>
@@ -813,8 +811,8 @@ function VersionPickerSheet({ plugin, versions, failed, busy, t, onPick, onClose
t: T; onPick: (version: string) => void; onClose: () => void
}) {
return (
<MSheet open onClose={onClose} variant="bottom" material="opaque" ariaLabel={t('admin.plugins.versionPickerTitle', { name: plugin.name })}>
<MAdminSheetFrame title={t('admin.plugins.versionPickerTitle', { name: plugin.name })} onClose={onClose}>
<MSheet open onClose={onClose} variant="bottom" material="glass" ariaLabel={t('admin.plugins.versionPickerTitle', { name: plugin.name })}>
<MAdminSheetFrame icon={<History size={19} strokeWidth={1.8} />} title={t('admin.plugins.versionPickerTitle', { name: plugin.name })} onClose={onClose}>
<div className="space-y-0.5">
{failed && <p className="px-2.5 py-2 text-xs text-[color:var(--m-st-danger)]">{t('admin.plugins.detailError')}</p>}
{!failed && !versions && <p className="px-2.5 py-2 text-xs text-m-faint">{t('common.loading')}</p>}
@@ -973,7 +971,7 @@ function PluginDetailSheet({ item, installed, busy, onInstall, onClose, t, local
const sectionH = 'text-[11px] font-semibold uppercase tracking-wider text-m-muted'
return (
<MSheet open onClose={onClose} variant="card" material="opaque" ariaLabel={item.name}>
<MSheet open onClose={onClose} variant="card" material="glass" ariaLabel={item.name}>
<div className="relative flex-none">
<Screenshot url={item.screenshotUrl} className="aspect-[16/9] w-full" iconSize={36} />
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
@@ -1195,7 +1193,7 @@ function SignatureBlockSheet({ data, entry, busy, t, onRetrust, onClose }: {
const offerRetrust = retrust !== null
return (
<MSheet open onClose={onClose} variant="card" material="opaque" ariaLabel={t('admin.plugins.sig.title', { name: data.subject.name })}>
<MSheet open onClose={onClose} variant="card" material="glass" ariaLabel={t('admin.plugins.sig.title', { name: data.subject.name })}>
<div className="flex-none px-[18px] pt-[18px]">
<div className="flex items-start gap-3">
<div className={`grid h-9 w-9 shrink-0 place-items-center rounded-lg ${CHIP_PENDING}`}><ShieldAlert size={18} /></div>
@@ -1248,7 +1246,7 @@ function UpdateConsentSheet({ data, unsigned, t, onApprove, onLater }: {
t: T; onApprove: () => void; onLater: () => void
}) {
return (
<MSheet open onClose={onLater} variant="card" material="opaque" ariaLabel={t('admin.plugins.updateConsentTitle')}>
<MSheet open onClose={onLater} variant="card" material="glass" ariaLabel={t('admin.plugins.updateConsentTitle')}>
<div className="flex-none px-[18px] pt-[18px]">
<div className="flex items-start gap-3">
<div className={`grid h-9 w-9 shrink-0 place-items-center rounded-lg ${CHIP_PENDING}`}><ShieldCheck size={18} /></div>
@@ -1305,7 +1303,7 @@ function DependencyResolveSheet({ data, t, busy, installedIds, onDownload, onClo
}) {
const rows = dependencyRows(data)
return (
<MSheet open onClose={onClose} variant="card" material="opaque" ariaLabel={t('admin.plugins.dep.resolveTitle')}>
<MSheet open onClose={onClose} variant="card" material="glass" ariaLabel={t('admin.plugins.dep.resolveTitle')}>
<div className="flex-none px-[18px] pt-[18px]">
<div className="flex items-start gap-3">
<div className={`grid h-9 w-9 shrink-0 place-items-center rounded-lg ${CHIP_PENDING}`}><Puzzle size={18} /></div>
@@ -32,7 +32,8 @@ export default function MAdminResetMfaSheet({ open, onClose, user, onReset, t }:
return (
<MSheet open={open} onClose={onClose} ariaLabel={t('admin.mfa.reset')}>
<MAdminSheetFrame
title={t('admin.mfa.reset')}
icon={<ShieldOff size={19} strokeWidth={1.8} />}
title={t('admin.mfa.reset')}
onClose={onClose}
footer={
<>
@@ -41,9 +41,10 @@ function AddCountrySheet({
onClose();
};
return (
<MSheet open={open} onClose={close} variant="card" material="opaque" ariaLabel={t('schoolCatalog.addCountry')}>
<MSheet open={open} onClose={close} variant="card" material="glass" ariaLabel={t('schoolCatalog.addCountry')}>
<MAdminSheetFrame
title={t('schoolCatalog.addCountry')}
icon={<GraduationCap size={19} strokeWidth={1.8} />}
title={t('schoolCatalog.addCountry')}
onClose={close}
footer={
<>
@@ -113,12 +114,13 @@ function RegionSheet({ region, busy, offline, error, onClose, onSave }: EditorPr
if (!busy) onClose();
}}
variant="card"
material="opaque"
material="glass"
ariaLabel={name.trim() || t('schoolCatalog.region')}
discardGuard={body}
>
<MAdminSheetFrame
title={name.trim() || t('schoolCatalog.region')}
icon={<GraduationCap size={19} strokeWidth={1.8} />}
title={name.trim() || t('schoolCatalog.region')}
onClose={() => {
if (!busy) onClose();
}}
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { AlertTriangle, CheckCircle, ExternalLink, Fingerprint, ShieldOff, Trash2 } from 'lucide-react'
import { AlertTriangle, CheckCircle, ExternalLink, Fingerprint, ShieldOff, Trash2, UserPlus, UserCog, Download, KeyRound, RotateCcw } from 'lucide-react'
import { useAdminUserActions } from '../../../components/Admin/useAdminUserActions'
import type { TranslationFn } from '../../../types'
import type { useAdmin } from '../../../pages/admin/useAdmin'
@@ -63,6 +63,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
{/* Create user */}
<MSheet open={showCreateUser} onClose={() => setShowCreateUser(false)} ariaLabel={t('admin.createUser')}>
<MAdminSheetFrame
icon={<UserPlus size={19} strokeWidth={1.8} />}
title={t('admin.createUser')}
onClose={() => setShowCreateUser(false)}
footer={
@@ -115,6 +116,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
{/* Edit user */}
<MSheet open={!!editingUser} onClose={() => setEditingUser(null)} ariaLabel={t('admin.editUser')}>
<MAdminSheetFrame
icon={<UserCog size={19} strokeWidth={1.8} />}
title={t('admin.editUser')}
onClose={() => setEditingUser(null)}
footer={
@@ -205,6 +207,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
{/* How to update */}
<MSheet open={showUpdateModal} onClose={() => setShowUpdateModal(false)} ariaLabel={t('admin.update.howTo')}>
<MAdminSheetFrame
icon={<Download size={19} strokeWidth={1.8} />}
title={t('admin.update.howTo')}
onClose={() => setShowUpdateModal(false)}
footer={<MAdminButton onClick={() => setShowUpdateModal(false)}>{t('common.close')}</MAdminButton>}
@@ -256,6 +259,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
{/* Reset passkeys confirm */}
<MSheet open={showResetPasskeys} onClose={() => setShowResetPasskeys(false)} ariaLabel={t('admin.passkey.reset')}>
<MAdminSheetFrame
icon={<KeyRound size={19} strokeWidth={1.8} />}
title={t('admin.passkey.reset')}
onClose={() => setShowResetPasskeys(false)}
footer={
@@ -288,6 +292,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
{/* Rotate JWT confirm */}
<MSheet open={showRotateJwtModal} onClose={() => setShowRotateJwtModal(false)} ariaLabel={t('admin.rotateJwt.title')}>
<MAdminSheetFrame
icon={<RotateCcw size={19} strokeWidth={1.8} />}
title={t('admin.rotateJwt.title')}
onClose={() => setShowRotateJwtModal(false)}
footer={
+17 -7
View File
@@ -83,7 +83,7 @@ export function MAdminClampedText({ children, className = '' }: { children: stri
export function MAdminField({ label, hint, children }: { label: ReactNode; hint?: ReactNode; children: ReactNode }) {
return (
<div>
<div className="mb-[6px] text-[0.75rem] font-semibold text-m-ink">{label}</div>
<div className="mb-[5px] font-geist text-[0.625rem] font-bold uppercase tracking-[.09em] text-m-faint">{label}</div>
{children}
{hint && <div className="mt-[5px] font-geist text-[0.625rem] leading-relaxed text-m-muted">{hint}</div>}
</div>
@@ -94,7 +94,7 @@ export function MAdminInput({ className = '', ...props }: InputHTMLAttributes<HT
return (
<input
{...props}
className={`h-[42px] w-full rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-3 text-[0.84375rem] text-m-ink outline-none placeholder:text-m-faint focus:border-[color:var(--m-faint)] disabled:cursor-not-allowed disabled:opacity-60 ${className}`}
className={`h-[42px] w-full rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[14px] text-[0.84375rem] text-m-ink outline-none placeholder:text-m-faint focus:border-[color:var(--m-faint)] disabled:cursor-not-allowed disabled:opacity-60 ${className}`}
/>
)
}
@@ -161,11 +161,14 @@ export function MAdminButton({
/** Sheet body frame: title row + close, scrollable content, optional footer. */
export function MAdminSheetFrame({
title,
icon,
onClose,
footer,
children,
}: {
title: ReactNode
/** The icon tile in front of the title, as every phone sheet has. */
icon?: ReactNode
onClose: () => void
footer?: ReactNode
children: ReactNode
@@ -173,15 +176,22 @@ export function MAdminSheetFrame({
const { t } = useTranslation()
return (
<>
<div className="flex flex-none items-center gap-2 px-4 pb-2 pt-4">
<div className="min-w-0 flex-1 truncate text-[0.9375rem] font-extrabold text-m-ink">{title}</div>
{/* TileHeader's shape: the icon tile, the title and the round close. */}
<div className="flex flex-none items-center gap-3 px-[18px] pb-1 pt-4">
{icon && (
<span className="flex h-10 w-10 flex-none items-center justify-center rounded-[13px] bg-[color:var(--m-ic)] text-m-ink">
{icon}
</span>
)}
<div className="min-w-0 flex-1 truncate text-[1.0625rem] font-bold leading-tight text-m-ink">{title}</div>
<MIconBtn variant="neutral" size={34} ariaLabel={t('common.close')} onClick={onClose}>
<X size={16} strokeWidth={2.2} />
<X size={15} strokeWidth={2.2} />
</MIconBtn>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">{children}</div>
<div className="m-scroll-fade mt-2 min-h-0 flex-1 overflow-y-auto px-[18px] pb-[18px] pt-[14px]">{children}</div>
{footer && (
<div className="flex flex-none items-center justify-end gap-2 border-t border-[color:var(--m-rowbr)] px-4 py-3">
// The footer's buttons take the sheets' 42 px size; elsewhere MAdminButton stays a small pill.
<div className="flex flex-none items-center justify-end gap-2 border-t border-[color:var(--m-rowbr)] px-[18px] pb-4 pt-3 [&>button]:h-[42px] [&>button]:px-4 [&>button]:text-[length:calc(13px*var(--fs-scale-body,1))] [&>button]:font-semibold">
{footer}
</div>
)}
@@ -1,4 +1,4 @@
import { Copy, Link2, Trash2, UserPlus } from 'lucide-react'
import { Copy, Link2, Trash2, UserPlus, Ticket } from 'lucide-react'
import type { TranslationFn } from '../../../types'
import type { useAdmin } from '../../../pages/admin/useAdmin'
import { inviteMeta, inviteStatus } from '../../../pages/admin/adminModel'
@@ -167,6 +167,7 @@ export default function MAdminUsersSection({ admin, t, locale }: MAdminUsersSect
{/* Create invite sheet */}
<MSheet open={showCreateInvite} onClose={() => setShowCreateInvite(false)} ariaLabel={t('admin.invite.create')}>
<MAdminSheetFrame
icon={<Ticket size={19} strokeWidth={1.8} />}
title={t('admin.invite.create')}
onClose={() => setShowCreateInvite(false)}
footer={
@@ -68,7 +68,7 @@ export default function MCollPlaceSheet({
'flex flex-1 items-center justify-center gap-[6px] rounded-[13px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheet)] px-2 py-[11px] text-[0.78125rem] font-semibold text-m-ink'
return (
<MSheet open={place != null} onClose={onClose} material="opaque" className="!rounded-[24px]" ariaLabel={held?.name}>
<MSheet open={place != null} onClose={onClose} material="glass" className="!rounded-[24px]" ariaLabel={held?.name}>
{held && (
<>
{/* Hero: auto cover (photo when available, the design gradient otherwise) */}
@@ -528,7 +528,7 @@ export default function MJourneyDetail({ journeyId, embedded = false }: JourneyD
{/* Opaque, not the glass default: these two sit over a full-screen map whose
card carousel is bright and busy, and the frosted material let the titles
behind read straight through the menu. */}
<MSheet open={showUploadMenu} onClose={() => setShowUploadMenu(false)} variant="bottom" material="opaque" ariaLabel={t('common.upload')}>
<MSheet open={showUploadMenu} onClose={() => setShowUploadMenu(false)} variant="bottom" material="glass" ariaLabel={t('common.upload')}>
<div className="flex flex-col gap-2 p-[10px]">
<MListRow
icon={Camera}
@@ -547,7 +547,7 @@ export default function MJourneyDetail({ journeyId, embedded = false }: JourneyD
</MSheet>
{/* Journey actions: book export, suggestions switch, settings */}
<MSheet open={showActionMenu} onClose={() => setShowActionMenu(false)} variant="bottom" material="opaque" ariaLabel={t('files.menu')}>
<MSheet open={showActionMenu} onClose={() => setShowActionMenu(false)} variant="bottom" material="glass" ariaLabel={t('files.menu')}>
<div className="flex flex-col gap-1 p-[10px]">
<div className="flex items-center gap-[11px] px-[10px] py-[11px]">
<EyeOff size={16} strokeWidth={2} className="flex-none text-m-muted" />
@@ -1,6 +1,8 @@
import { ReactNode } from 'react'
import { Check } from 'lucide-react'
import MSheet from '../../components/MSheet'
import { TileHeader } from '../trip/sheets/MTripSheetUi'
import { useTranslation } from '../../../i18n'
export interface MSetPickerOption {
value: string
@@ -21,10 +23,14 @@ interface MSetPickerSheetProps {
* presets). Selecting closes the sheet.
*/
export default function MSetPickerSheet({ open, onClose, title, options, value, onSelect }: MSetPickerSheetProps) {
const { t } = useTranslation()
const closeLabel = t('common.close')
return (
<MSheet open={open} onClose={onClose} variant="bottom" material="opaque" ariaLabel={title}>
<div className="px-[14px] pb-2 pt-4 text-[0.875rem] font-extrabold text-m-ink">{title}</div>
<div className="min-h-0 overflow-y-auto px-2 pb-3">
<MSheet open={open} onClose={onClose} variant="bottom" material="glass" ariaLabel={title}>
<div className="flex-none px-[18px] pt-4">
<TileHeader title={title} onClose={onClose} closeLabel={closeLabel} />
</div>
<div className="m-scroll-fade mt-1 min-h-0 overflow-y-auto px-2 pb-3 pt-2">
{options.map((opt) => {
const active = opt.value === value
return (
@@ -300,7 +300,7 @@ export default function MSettingsMcp() {
if (!createdClient) setCreateOpen(false)
}}
variant="card"
material="opaque"
material="glass"
ariaLabel={t('settings.oauth.modal.createTitle')}
>
<div className="min-h-0 overflow-y-auto p-[18px]">
@@ -409,7 +409,7 @@ export default function MSettingsMcp() {
if (!createdToken) setTokenModalOpen(false)
}}
variant="card"
material="opaque"
material="glass"
ariaLabel={t('settings.mcp.modal.createTitle')}
>
<div className="p-[18px]">
@@ -461,7 +461,7 @@ export default function MSettingsMcp() {
</MSheet>
{/* Rotated secret display */}
<MSheet open={rotatedSecret !== null} onClose={() => setRotatedSecret(null)} variant="card" material="opaque" ariaLabel={t('settings.oauth.rotateSecretDoneTitle')}>
<MSheet open={rotatedSecret !== null} onClose={() => setRotatedSecret(null)} variant="card" material="glass" ariaLabel={t('settings.oauth.rotateSecretDoneTitle')}>
<div className="p-[18px]">
<div className="text-[0.9375rem] font-extrabold text-m-ink">{t('settings.oauth.rotateSecretDoneTitle')}</div>
<p className="mt-2 rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] p-3 font-geist text-[0.625rem] leading-relaxed text-[color:var(--m-st-pending)]">
@@ -1,6 +1,6 @@
import { MapPin, X } from 'lucide-react'
import { MapPin, Tag } from 'lucide-react'
import MSheet from '../../../components/MSheet'
import MIconBtn from '../../../components/MIconBtn'
import { TileHeader } from '../sheets/MTripSheetUi'
import { getCategoryIcon } from '../../../../components/shared/categoryIcons'
import { useTranslation } from '../../../../i18n'
import type { Category } from '../../../../types'
@@ -21,17 +21,17 @@ interface MPlacesBulkCategorySheetProps {
export default function MPlacesBulkCategorySheet({ open, count, categories, onPick, onClose }: MPlacesBulkCategorySheetProps) {
const { t } = useTranslation()
return (
<MSheet open={open} onClose={onClose} variant="card" ariaLabel={t('places.changeCategory')}>
<div className="flex flex-none items-center border-b border-[color:var(--m-rowbr)] px-[18px] pb-[11px] pt-4">
<div className="min-w-0 flex-1">
<div className="text-[1.03125rem] font-bold text-m-ink">{t('places.changeCategory')}</div>
<div className="mt-[2px] font-geist text-[0.6875rem] text-m-muted">{t('places.selectionCount', { count })}</div>
</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('places.changeCategory')}>
<div className="flex-none px-[18px] pt-4">
<TileHeader
icon={<Tag size={19} strokeWidth={1.8} />}
title={t('places.changeCategory')}
sub={t('places.selectionCount', { count })}
onClose={onClose}
closeLabel={t('common.close')}
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-[10px] pb-3 pt-[6px]">
<div className="m-scroll-fade mt-2 min-h-0 flex-1 overflow-y-auto px-[10px] pb-3 pt-[10px]">
{categories.map(c => {
const CatIcon = getCategoryIcon(c.icon)
return (
@@ -1,6 +1,6 @@
import { ArrowRight, Bookmark, Loader2, Plus, X } from 'lucide-react'
import { ArrowRight, Bookmark, Loader2, Plus } from 'lucide-react'
import MSheet from '../../../components/MSheet'
import MIconBtn from '../../../components/MIconBtn'
import { TileHeader } from '../sheets/MTripSheetUi'
import { useTranslation } from '../../../../i18n'
import { useSaveTripPlacesToList } from '../../../../components/Collections/useSaveTripPlacesToList'
@@ -25,14 +25,14 @@ export default function MPlacesSaveToCollectionSheet({ open, tripId, placeIds, o
})
return (
<MSheet open={open} onClose={onClose} variant="card" ariaLabel={t('collections.saveNToList', { count: placeIds.length })}>
<div className="flex flex-none items-center border-b border-[color:var(--m-rowbr)] px-[18px] pb-[11px] pt-4">
<div className="min-w-0 flex-1 text-[1.03125rem] font-bold text-m-ink">
{t('collections.saveNToList', { count: placeIds.length })}
</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('collections.saveNToList', { count: placeIds.length })}>
<div className="flex-none px-[18px] pt-4">
<TileHeader
icon={<Bookmark size={19} strokeWidth={1.8} />}
title={t('collections.saveNToList', { count: placeIds.length })}
onClose={onClose}
closeLabel={t('common.close')}
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-[14px] pb-[14px] pt-1">
{lists.length > 5 && (
@@ -74,7 +74,7 @@ export default function MRtCorridorSheet({ planner, shell }: MTripSheetsProps) {
open={open}
onClose={shell.closeSheet}
variant="bottom"
material="opaque"
material="glass"
ariaLabel={t('roadtrip.poi.title')}
>
<div className="flex-none px-[18px] pt-4">
@@ -75,7 +75,7 @@ export default function MRtDraftSheet({ planner }: MRtDraftSheetProps) {
}
return (
<MSheet open={stopDraft != null} onClose={discard} variant="bottom" material="opaque" ariaLabel={draft.poi.name}>
<MSheet open={stopDraft != null} onClose={discard} variant="bottom" material="glass" ariaLabel={draft.poi.name}>
<FormSheetHeader
icon={kind?.Icon ?? ParkingSquare}
title={draft.poi.name}
@@ -172,7 +172,7 @@ export default function MRtInfoSheet({ planner, shell }: MTripSheetsProps) {
]
return (
<MSheet open={open} onClose={shell.closeSheet} variant="bottom" material="opaque" ariaLabel={t('mobileTrip.rtInfoTitle')}>
<MSheet open={open} onClose={shell.closeSheet} variant="bottom" material="glass" ariaLabel={t('mobileTrip.rtInfoTitle')}>
<div className="flex-none px-[18px] pt-4">
<TileHeader
icon={<VehicleIcon size={19} strokeWidth={1.8} />}
@@ -81,7 +81,7 @@ export default function MRtKindSheet({ planner, shell }: MTripSheetsProps) {
open={open && !!stop}
onClose={shell.closeSheet}
variant="bottom"
material="opaque"
material="glass"
ariaLabel={t('roadtrip.stop.kind')}
>
{stop && (
@@ -150,7 +150,7 @@ export default function MRtStaySheet({ planner, shell }: MTripSheetsProps) {
open={open && !!stop}
onClose={shell.closeSheet}
variant="bottom"
material="opaque"
material="glass"
// Named for what it holds: a stop left at a set time has no stay to add.
ariaLabel={leave.until ? t('roadtrip.stop.stay') : t('roadtrip.stay.add')}
>
@@ -290,7 +290,7 @@ export default function MRtStopSheet({ planner, shell }: MTripSheetsProps) {
const tiles = (navTargets.length > 0 ? 1 : 0) + 1
return (
<MSheet open={open && !!live} onClose={shell.closeSheet} variant="bottom" material="opaque" ariaLabel={stop.name}>
<MSheet open={open && !!live} onClose={shell.closeSheet} variant="bottom" material="glass" ariaLabel={stop.name}>
{/* ── Header: the disc the chain draws, the name, the way to the editor and the way out ── */}
<div className="flex-none px-[18px] pt-4">
{/* Centred, not top-aligned: one line of the name is shorter than the 34px disc, so
@@ -122,7 +122,7 @@ export default function MCostSheet({ tripId, base, people, me, editing, prefill,
<MSheet
open={open}
onClose={requestClose}
material="opaque"
material="glass"
ariaLabel={editing ? t('costs.editExpense') : t('costs.addExpense')}
>
<FormSheetHeader
@@ -233,7 +233,7 @@ export default function MPlaceEditSheet({ planner, onOpenExpense }: MPlaceEditSh
<MSheet
open={showPlaceForm}
onClose={handleClose}
material="opaque"
material="glass"
ariaLabel={sheetPlace ? t('places.editPlace') : t('places.addPlace')}
discardGuard={showPlaceForm ? { form, files: pendingFiles.length } : undefined}
>
@@ -175,7 +175,7 @@ export default function MReservationSheet({ planner, onOpenExpense }: MReservati
<MSheet
open={showReservationModal}
onClose={handleClose}
material="opaque"
material="glass"
ariaLabel={res ? t('reservations.editTitle') : t('reservations.newTitle')}
discardGuard={showReservationModal ? { form, files: pendingFiles.length, travelers: [...travelerIds] } : undefined}
>
@@ -163,7 +163,7 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
<MSheet
open={showTransportModal}
onClose={handleClose}
material="opaque"
material="glass"
ariaLabel={headerTitle}
discardGuard={showTransportModal ? { form, waypoints, trainWaypoints, carStops, files: pendingFiles.length, travelers: [...travelerIds] } : undefined}
>
@@ -76,7 +76,8 @@ export function SheetSection({
}
interface TileHeaderProps {
icon: ReactNode
/** Left out for a plain picker; every other sheet shows its icon tile. */
icon?: ReactNode
title: ReactNode
sub?: ReactNode
/** Let a sentence-length subtitle wrap instead of truncating it to one line. */
@@ -97,9 +98,11 @@ interface TileHeaderProps {
export function TileHeader({ icon, title, sub, subWrap = false, onClose, closeLabel }: TileHeaderProps) {
return (
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-[13px] bg-[color:var(--m-ic)]">
{icon}
</div>
{icon && (
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-[13px] bg-[color:var(--m-ic)]">
{icon}
</div>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-[6px] text-[1.0625rem] font-bold leading-tight">{title}</div>
{sub && (