mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
fix(mobile): the tour tab's rail heads are rounded bands like the cards under them
This commit is contained in:
@@ -7,7 +7,8 @@ import { useSettingsStore } from '../../../store/settingsStore'
|
||||
import { useTourFerrataCrossingStore } from '../../../store/tourFerrataCrossingStore'
|
||||
import { formatDistance } from '../../../utils/units'
|
||||
import ConfirmDialog from '../../shared/ConfirmDialog'
|
||||
import { NEUTRAL_TINT, fs } from '../../shared/DialogShell'
|
||||
import { fs } from '../../shared/DialogShell'
|
||||
import { useRailHeadProps } from './railHead'
|
||||
import EmptyState from '../../shared/EmptyState'
|
||||
import { useToast } from '../../shared/Toast'
|
||||
import { Tooltip } from '../../shared/Tooltip'
|
||||
@@ -74,9 +75,6 @@ import { SkiPlannerSummary } from '../ski/SkiRouteSummary'
|
||||
import { walkTimeFields } from '../walk/walkTimeFields'
|
||||
|
||||
/** The tinted head band both rails open with, like every panel head in the planner. */
|
||||
export const RAIL_HEAD = 'flex flex-none items-start gap-2 border-b border-edge-faint px-3 py-2.5'
|
||||
/** The head as a rounded band of its own, inset like the controls above it (the touch shell's tours tab). */
|
||||
const FLOATING_RAIL_HEAD = 'mx-4 mt-1 flex flex-none items-start gap-2 rounded-xl border border-edge-faint px-3 py-2'
|
||||
|
||||
/** The title of a rail's head band, with an optional line under it. */
|
||||
export function RailTitle({ title, sub, aside }: { title: string; sub?: ReactNode; aside?: ReactNode }) {
|
||||
@@ -138,10 +136,11 @@ function NeutralRail({
|
||||
canEdit: boolean
|
||||
onOpenWanderer?: () => void
|
||||
}) {
|
||||
const railHead = useRailHeadProps()
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<section className="flex h-full min-h-0 flex-col" aria-label={t('tours.planner.title')}>
|
||||
<div className={RAIL_HEAD} style={{ background: NEUTRAL_TINT }}>
|
||||
<div {...railHead}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canEdit}
|
||||
@@ -209,6 +208,7 @@ function GpxViewRail({
|
||||
paddleChecklist?: PaddleChecklist
|
||||
skiClassification?: SkiClassification
|
||||
}) {
|
||||
const railHead = useRailHeadProps()
|
||||
const { t } = useTranslation()
|
||||
const distanceUnit = useSettingsStore((state) => state.settings.distance_unit)
|
||||
const { readOnlyGpxTour } = planner
|
||||
@@ -228,7 +228,7 @@ function GpxViewRail({
|
||||
return (
|
||||
<>
|
||||
<section className="flex h-full min-h-0 flex-col" aria-label={tour.name}>
|
||||
<div className={RAIL_HEAD} style={{ background: NEUTRAL_TINT }}>
|
||||
<div {...railHead}>
|
||||
<BarButton
|
||||
label={t('tours.planner.backToTours')}
|
||||
onClick={() => planner.returnToNeutral({ keepView: true })}
|
||||
@@ -330,6 +330,7 @@ export function TourPlannerRail({ planner,
|
||||
/** The trip's tours, never offered as a stop of another tour (PR1-10). */
|
||||
tourPlaceIds?: ReadonlySet<number>
|
||||
} & TourPermissionProps) {
|
||||
const railHead = useRailHeadProps()
|
||||
const { canEdit } = useTourPermissions({ canEdit: editPermission ?? planner.canEdit, canAssign: assignPermission ?? planner.canAssign })
|
||||
const { t } = useTranslation()
|
||||
const [confirmDiscard, setConfirmDiscard] = useState(false)
|
||||
@@ -366,7 +367,7 @@ export function TourPlannerRail({ planner,
|
||||
return (
|
||||
<>
|
||||
<section className="flex h-full min-h-0 flex-col" aria-label={t('tours.planner.title')}>
|
||||
<div className={RAIL_HEAD} style={{ background: NEUTRAL_TINT }}>
|
||||
<div {...railHead}>
|
||||
<BarButton label={t('tours.planner.backToTours')} onClick={() => requestClose()} disabled={isSaving} className="focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent">
|
||||
<ArrowLeft size={16} strokeWidth={2} />
|
||||
</BarButton>
|
||||
@@ -568,8 +569,6 @@ interface TourPlannerToursRailProps extends TourPermissionProps {
|
||||
gpxImport?: TourGpxImport
|
||||
/** LIB-02: takes a tour of another trip in, an entry of "Add tour". */
|
||||
onTakenOver?: (tour: TourListItem) => Promise<void>
|
||||
/** The head as a rounded band rather than a ruled one (the touch shell's tours tab). */
|
||||
floatingHead?: boolean
|
||||
}
|
||||
|
||||
/** The words of the time estimate: walking, riding (BIK-01), running (RUN-01) or paddling, each saying how it is reckoned, and a ride none. */
|
||||
@@ -676,7 +675,8 @@ function stagesFold(tour: TourListItem, rail: ReturnType<typeof useTourStageRail
|
||||
}
|
||||
|
||||
export function TourPlannerToursRail({ planner, tours, days, loading, onAssignToDay, onViewGpxTour, onDeleteTour,
|
||||
gpxImport, onTakenOver, canEdit: editPermission, canAssign: assignPermission, floatingHead = false }: TourPlannerToursRailProps) {
|
||||
gpxImport, onTakenOver, canEdit: editPermission, canAssign: assignPermission }: TourPlannerToursRailProps) {
|
||||
const railHead = useRailHeadProps()
|
||||
const { canEdit, canAssign } = useTourPermissions({ canEdit: editPermission ?? planner.canEdit, canAssign: assignPermission ?? planner.canAssign })
|
||||
const { t } = useTranslation()
|
||||
const toast = useToast()
|
||||
@@ -738,7 +738,7 @@ export function TourPlannerToursRail({ planner, tours, days, loading, onAssignTo
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-h-0 flex-col" aria-label={t('tours.planner.tripTours')}>
|
||||
<div className={floatingHead ? FLOATING_RAIL_HEAD : RAIL_HEAD} style={{ background: NEUTRAL_TINT }}>
|
||||
<div {...railHead}>
|
||||
<RailTitle title={t('tours.planner.tripTours')} />
|
||||
{canEdit && <TourAddMenu planner={planner} gpxImport={gpxImport} onTakenOver={onTakenOver} disabled={isSaving} />}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render, screen } from '../../../../tests/helpers/render';
|
||||
import { FloatingRailHeads, RAIL_HEAD, useRailHeadProps } from './railHead';
|
||||
|
||||
function Head() {
|
||||
return <div data-testid="head" {...useRailHeadProps()} />;
|
||||
}
|
||||
|
||||
describe('rail heads', () => {
|
||||
it('FE-TOUR-RAILHEAD-001: a ruled, tinted strip on the desktop, a rounded card-like band in the touch tab', () => {
|
||||
const { unmount } = render(<Head />);
|
||||
expect(screen.getByTestId('head').className).toBe(RAIL_HEAD);
|
||||
expect(screen.getByTestId('head').getAttribute('style')).toContain('background');
|
||||
unmount();
|
||||
|
||||
render(
|
||||
<FloatingRailHeads value>
|
||||
<Head />
|
||||
</FloatingRailHeads>
|
||||
);
|
||||
const head = screen.getByTestId('head');
|
||||
expect(head.className).toContain('rounded-2xl');
|
||||
expect(head.className).not.toContain('border-b');
|
||||
expect(head.getAttribute('style')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createContext, use } from 'react';
|
||||
import { NEUTRAL_TINT } from '../../shared/DialogShell';
|
||||
|
||||
/** The head band of a tours rail: ruled off from the rail under it, as the desktop planner shows it. */
|
||||
export const RAIL_HEAD = 'flex flex-none items-start gap-2 border-b border-edge-faint px-3 py-2.5';
|
||||
|
||||
/** The head as a rounded band of its own, inset and shaped like the cards under it. */
|
||||
const FLOATING_RAIL_HEAD =
|
||||
'mx-3 mt-3 flex flex-none items-start gap-2 rounded-2xl border border-edge-faint bg-surface-secondary px-3 py-2';
|
||||
|
||||
/**
|
||||
* Set by the touch shell's tours tab: there each rail's head (the trip's tours,
|
||||
* a new or saved tour, a GPX track, the wanderer trails) is a rounded band like
|
||||
* the cards below it rather than a ruled strip across the pane.
|
||||
*/
|
||||
export const FloatingRailHeads = createContext(false);
|
||||
|
||||
/** The class and style of a rail's head band, as the surrounding tab wants it. */
|
||||
export function useRailHeadProps(): { className: string; style?: { background: string } } {
|
||||
return use(FloatingRailHeads)
|
||||
? { className: FLOATING_RAIL_HEAD }
|
||||
: { className: RAIL_HEAD, style: { background: NEUTRAL_TINT } };
|
||||
}
|
||||
@@ -24,10 +24,11 @@ import { useSettingsStore } from '../../../store/settingsStore';
|
||||
import { formatDisplayDate } from '../../../utils/dateFormat';
|
||||
import { formatDistance, formatElevation } from '../../../utils/units';
|
||||
import { BarButton, WhiteBadge } from '../../Planner/planParts';
|
||||
import { DialogButton, NEUTRAL_TINT, fs } from '../../shared/DialogShell';
|
||||
import { DialogButton, fs } from '../../shared/DialogShell';
|
||||
import EmptyState from '../../shared/EmptyState';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { RAIL_HEAD, RailTitle } from '../planner/TourPlannerPanels';
|
||||
import { useRailHeadProps } from '../planner/railHead';
|
||||
import { RailTitle } from '../planner/TourPlannerPanels';
|
||||
import { TourDayRowAvatar } from '../TourDayRowFacts';
|
||||
import { TourListCard, TourMetricFields, TourNotice, TourSection } from '../tourParts';
|
||||
import { formatPlannedTourDuration } from '../tourPresentation';
|
||||
@@ -40,11 +41,12 @@ import WandererMark from './WandererMark';
|
||||
* The trail itself shows on the map between the rails.
|
||||
*/
|
||||
export default function WandererBrowserRail({ browser }: { browser: WandererBrowser }) {
|
||||
const railHead = useRailHeadProps();
|
||||
const { t } = useTranslation();
|
||||
const inPreview = browser.selected !== null;
|
||||
return (
|
||||
<section className="flex h-full min-h-0 flex-col" aria-label={t('wanderer.browser.title')}>
|
||||
<div className={RAIL_HEAD} style={{ background: NEUTRAL_TINT }}>
|
||||
<div {...railHead}>
|
||||
<BarButton
|
||||
label={inPreview ? t('wanderer.browser.backToList') : t('tours.planner.backToTours')}
|
||||
onClick={inPreview ? browser.backToList : browser.close}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createPortal } from 'react-dom';
|
||||
import TourGpxDropZone from '../../../../components/Tours/TourGpxDropZone';
|
||||
import TourWorkspaceRail from '../../../../components/Tours/TourWorkspaceRail';
|
||||
import { TourPlannerToursRail } from '../../../../components/Tours/planner/TourPlannerPanels';
|
||||
import { FloatingRailHeads } from '../../../../components/Tours/planner/railHead';
|
||||
import { useIsSplit } from '../../../../hooks/sizeClass';
|
||||
import { useTouchDragBridge } from '../../../../hooks/useTouchDragBridge';
|
||||
import { LazyPanel } from '../../../../pages/tripPlanner/LazyPanel';
|
||||
@@ -105,30 +106,32 @@ export default function MTourWorkspace({ planner }: Pick<MTripTabPanelProps, 'pl
|
||||
className="flex h-full min-w-0 flex-1 flex-col split:w-[var(--trip-pane-open)] split:flex-none split:border-e split:border-[color:var(--m-rowbr)]"
|
||||
>
|
||||
{!headSlot && <div className="flex items-center gap-2 px-4 pb-2 pt-1">{head}</div>}
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden pb-[env(safe-area-inset-bottom,0px)]">
|
||||
{pane === 'tours' ? (
|
||||
<LazyPanel id="m-tour-tours">
|
||||
<TourPlannerToursRail
|
||||
planner={tourPlanner}
|
||||
canEdit={canPlaceEdit}
|
||||
canAssign={canDayEdit}
|
||||
tours={planner.tours}
|
||||
days={planner.days}
|
||||
loading={planner.toursLoading}
|
||||
onAssignToDay={planner.handleAssignToDay}
|
||||
onDeleteTour={planner.handleDeleteTour}
|
||||
gpxImport={tourGpxImport}
|
||||
onViewGpxTour={page.viewImportedTour}
|
||||
onTakenOver={tourDetailActions.onTakenOver}
|
||||
floatingHead
|
||||
/>
|
||||
</LazyPanel>
|
||||
) : (
|
||||
<LazyPanel id="m-tour-editor">
|
||||
<TourWorkspaceRail workspace={page} canEdit={canPlaceEdit} canAssign={canDayEdit} />
|
||||
</LazyPanel>
|
||||
)}
|
||||
</div>
|
||||
{/* Every rail head here is a rounded band like the cards under it. */}
|
||||
<FloatingRailHeads value>
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden pb-[env(safe-area-inset-bottom,0px)]">
|
||||
{pane === 'tours' ? (
|
||||
<LazyPanel id="m-tour-tours">
|
||||
<TourPlannerToursRail
|
||||
planner={tourPlanner}
|
||||
canEdit={canPlaceEdit}
|
||||
canAssign={canDayEdit}
|
||||
tours={planner.tours}
|
||||
days={planner.days}
|
||||
loading={planner.toursLoading}
|
||||
onAssignToDay={planner.handleAssignToDay}
|
||||
onDeleteTour={planner.handleDeleteTour}
|
||||
gpxImport={tourGpxImport}
|
||||
onViewGpxTour={page.viewImportedTour}
|
||||
onTakenOver={tourDetailActions.onTakenOver}
|
||||
/>
|
||||
</LazyPanel>
|
||||
) : (
|
||||
<LazyPanel id="m-tour-editor">
|
||||
<TourWorkspaceRail workspace={page} canEdit={canPlaceEdit} canAssign={canDayEdit} />
|
||||
</LazyPanel>
|
||||
)}
|
||||
</div>
|
||||
</FloatingRailHeads>
|
||||
</section>
|
||||
)}
|
||||
{showMap && (
|
||||
|
||||
Reference in New Issue
Block a user