mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(ui): point import controls inward and give Google Maps its own mark
Every import button in the client wore an outgoing arrow, so the control that pulls a booking or a place list in looked like it was about to send something out. The places-import sheet managed to contradict itself inside one screen: Download in the header, Upload on the row below it, Upload again on the file step. Imports now take Download, or FileDown where the button opens a file picker; genuine uploads and the desktop drop zones keep Upload. The mobile places-import entry point wore an ellipsis, which promises a menu. The "open in Google Maps" buttons drew the four-segment Google *search* G, flattened to one colour — a glyph that reads as "run a search". They get a map pin instead, factored into one component so the desktop and mobile copies stop drifting apart. Closes #2005 Closes #2006
This commit is contained in:
@@ -447,7 +447,7 @@ describe('PackingListPanel', () => {
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[]} />);
|
||||
|
||||
// Click the Import button (Upload icon in the header)
|
||||
const importBtn = container.querySelector('svg.lucide-upload')?.closest('button');
|
||||
const importBtn = container.querySelector('svg.lucide-download')?.closest('button');
|
||||
expect(importBtn).toBeTruthy();
|
||||
await user.click(importBtn!);
|
||||
|
||||
@@ -538,7 +538,7 @@ describe('PackingListPanel', () => {
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[]} />);
|
||||
|
||||
// Open import modal
|
||||
const importBtn = container.querySelector('svg.lucide-upload')?.closest('button');
|
||||
const importBtn = container.querySelector('svg.lucide-download')?.closest('button');
|
||||
await user.click(importBtn!);
|
||||
await screen.findByText('Import Packing List');
|
||||
|
||||
@@ -655,7 +655,7 @@ describe('PackingListPanel', () => {
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[]} />);
|
||||
|
||||
// Open import modal
|
||||
const importBtn = container.querySelector('svg.lucide-upload')?.closest('button');
|
||||
const importBtn = container.querySelector('svg.lucide-download')?.closest('button');
|
||||
await user.click(importBtn!);
|
||||
await screen.findByText('Import Packing List');
|
||||
|
||||
@@ -929,7 +929,7 @@ describe('PackingListPanel', () => {
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[]} />);
|
||||
|
||||
// Open import modal
|
||||
const importBtn = container.querySelector('svg.lucide-upload')?.closest('button');
|
||||
const importBtn = container.querySelector('svg.lucide-download')?.closest('button');
|
||||
await user.click(importBtn!);
|
||||
await screen.findByText('Import Packing List');
|
||||
|
||||
@@ -1215,7 +1215,7 @@ describe('PackingListPanel', () => {
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[]} />);
|
||||
|
||||
// Open import modal
|
||||
const importBtn = container.querySelector('svg.lucide-upload')?.closest('button');
|
||||
const importBtn = container.querySelector('svg.lucide-download')?.closest('button');
|
||||
await user.click(importBtn!);
|
||||
await screen.findByText('Import Packing List');
|
||||
|
||||
@@ -1500,7 +1500,7 @@ describe('PackingListPanel', () => {
|
||||
const { container } = render(<PackingListPanel tripId={1} items={[]} />);
|
||||
|
||||
// Open import modal
|
||||
const importBtn = container.querySelector('svg.lucide-upload')?.closest('button');
|
||||
const importBtn = container.querySelector('svg.lucide-download')?.closest('button');
|
||||
await user.click(importBtn!);
|
||||
await screen.findByText('Import Packing List');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
X, Check, CheckCheck, Luggage, Package, FolderPlus, Upload,
|
||||
X, Check, CheckCheck, Luggage, Package, FolderPlus, Download,
|
||||
} from 'lucide-react'
|
||||
import type { PackingState } from './usePackingListPanel'
|
||||
|
||||
@@ -46,7 +46,7 @@ export function PackingHeader(S: PackingState) {
|
||||
border: '1px solid var(--border-primary)', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 500, cursor: 'pointer',
|
||||
fontFamily: 'inherit', background: 'var(--bg-card)', color: 'var(--text-muted)',
|
||||
}}>
|
||||
<Upload size={12} /> <span className="hidden sm:inline">{t('packing.import')}</span>
|
||||
<Download size={12} /> <span className="hidden sm:inline">{t('packing.import')}</span>
|
||||
</button>
|
||||
)}
|
||||
{inlineHeader && canEdit && abgehakt > 0 && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Upload } from 'lucide-react'
|
||||
import { FileDown } from 'lucide-react'
|
||||
import type { PackingState } from './usePackingListPanel'
|
||||
|
||||
export function BulkImportModal(S: PackingState) {
|
||||
@@ -48,7 +48,7 @@ export function BulkImportModal(S: PackingState) {
|
||||
border: '1px dashed var(--border-primary)', borderRadius: 8, background: 'none',
|
||||
fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>
|
||||
<Upload size={11} /> {t('packing.importCsv')}
|
||||
<FileDown size={11} /> {t('packing.importCsv')}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ChevronDown, ChevronRight, ChevronUp, Compass, Navigation, RotateCcw, E
|
||||
import { type PickedPlace } from './TransitSearchPanel'
|
||||
import { assignmentsApi, reservationsApi, daysApi } from '../../api/client'
|
||||
import { calculateRouteWithLegs, optimizeRoute, generateGoogleMapsUrl, generateCoMapsUrl, type NamedWaypoint } from '../Map/RouteCalculator'
|
||||
import GoogleMapsIcon from '../shared/GoogleMapsIcon'
|
||||
import PlaceAvatar from '../shared/PlaceAvatar'
|
||||
import ConfirmDialog from '../shared/ConfirmDialog'
|
||||
import { useContextMenu, ContextMenu } from '../shared/ContextMenu'
|
||||
@@ -2583,12 +2584,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
cursor: 'pointer', fontFamily: 'inherit', flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 48 48" fill="currentColor" aria-hidden="true">
|
||||
<path d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" />
|
||||
<path d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" />
|
||||
<path d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z" />
|
||||
<path d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" />
|
||||
</svg>
|
||||
<GoogleMapsIcon size={14} />
|
||||
</button>
|
||||
{/* The same day, handed to CoMaps for offline navigation (#1904). The
|
||||
day's own travel mode rides along, so the route it builds walks
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Search, Plus, X, Upload, ChevronDown, Check, MapPin, Star } from 'lucide-react'
|
||||
import { Search, Plus, X, Upload, FileDown, ChevronDown, Check, MapPin, Star } from 'lucide-react'
|
||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||
import Tooltip from '../shared/Tooltip'
|
||||
import type { SidebarState } from './usePlacesSidebar'
|
||||
@@ -54,7 +54,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
<Upload size={11} strokeWidth={2} /> {t('places.importFile')}
|
||||
<FileDown size={11} strokeWidth={2} /> {t('places.importFile')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setListImportOpen(true)}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render } from '@testing-library/react'
|
||||
import GoogleMapsIcon from './GoogleMapsIcon'
|
||||
|
||||
describe('GoogleMapsIcon', () => {
|
||||
it('FE-COMP-GMAPS-001: renders a pin, not the four-segment Google search "G" (#2005)', () => {
|
||||
const { container } = render(<GoogleMapsIcon />)
|
||||
const paths = container.querySelectorAll('path')
|
||||
expect(paths).toHaveLength(1)
|
||||
// The old mark was four paths lifted from the Google Search logo.
|
||||
expect(container.innerHTML).not.toContain('M24 9.5c3.54')
|
||||
})
|
||||
|
||||
it('FE-COMP-GMAPS-002: sizes itself and inherits the button colour', () => {
|
||||
const { container } = render(<GoogleMapsIcon size={19} />)
|
||||
const svg = container.querySelector('svg') as SVGElement
|
||||
expect(svg.getAttribute('width')).toBe('19')
|
||||
expect(svg.getAttribute('height')).toBe('19')
|
||||
expect(svg.getAttribute('fill')).toBe('currentColor')
|
||||
})
|
||||
|
||||
it('FE-COMP-GMAPS-003: stays out of the accessibility tree — the button carries the label', () => {
|
||||
const { container } = render(<GoogleMapsIcon />)
|
||||
const svg = container.querySelector('svg') as SVGElement
|
||||
expect(svg.getAttribute('aria-hidden')).toBe('true')
|
||||
expect(svg.getAttribute('focusable')).toBe('false')
|
||||
})
|
||||
|
||||
it('FE-COMP-GMAPS-004: passes className and style through', () => {
|
||||
const { container } = render(<GoogleMapsIcon className="text-m-faint" style={{ opacity: 0.5 }} />)
|
||||
const svg = container.querySelector('svg') as SVGElement
|
||||
expect(svg.getAttribute('class')).toBe('text-m-faint')
|
||||
expect(svg.style.opacity).toBe('0.5')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
/**
|
||||
* The map marker for the "open this day in Google Maps" buttons (#2005).
|
||||
*
|
||||
* What sat here before was the four-segment Google "G" — the *search* mark —
|
||||
* flattened to a single colour, which read as "run a Google search" rather than
|
||||
* "open a map". This is the pin instead: filled, so it is not mistaken for the
|
||||
* outline `MapPin` that labels ordinary places, and monochrome on currentColor
|
||||
* so it sits next to the lucide `Compass` of the CoMaps button beside it
|
||||
* without one of the two shouting. The button's own label carries the brand.
|
||||
*
|
||||
* Same shape as the other hand-vendored marks in the client (`BrandIcon` in
|
||||
* SystemNoticeModal, `ImmichIcon`/`SynologyIcon` in AddonManager): 24×24 box,
|
||||
* `fill="currentColor"`, `aria-hidden` because the button is already labelled.
|
||||
*/
|
||||
export default function GoogleMapsIcon({ size = 14, className, style }: {
|
||||
size?: number
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
style={style}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
{/* Teardrop pin with the hole punched out by evenodd, so the glyph stays
|
||||
readable at the 13-14px the toolbars render it at. */}
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 1.5c-4.142 0-7.5 3.358-7.5 7.5 0 5.25 6.09 12.34 6.87 13.222a.84.84 0 0 0 1.26 0C13.41 21.34 19.5 14.25 19.5 9c0-4.142-3.358-7.5-7.5-7.5Zm0 10.25a2.75 2.75 0 1 1 0-5.5 2.75 2.75 0 0 1 0 5.5Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Bookmark, Check, CheckCheck, CheckCircle2, ListChecks, Loader2, MapPin, MoreHorizontal, Plus,
|
||||
Bookmark, Check, CheckCheck, CheckCircle2, Download, ListChecks, Loader2, MapPin, Plus,
|
||||
SlidersHorizontal, Tag, Trash2, X,
|
||||
} from 'lucide-react'
|
||||
import MDancingTrek from '../../../components/MDancingTrek'
|
||||
@@ -172,7 +172,7 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
|
||||
aria-label={t('mobileTrip.importPlaces')}
|
||||
className="flex h-10 w-10 flex-none items-center justify-center rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] text-m-muted"
|
||||
>
|
||||
<MoreHorizontal size={16} strokeWidth={2} />
|
||||
<Download size={16} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { Check, Upload } from 'lucide-react'
|
||||
import { Check, FileDown } from 'lucide-react'
|
||||
import { placesApi } from '../../../../api/client'
|
||||
import { Eyebrow, FormSheetFooter } from './PlSheetChrome'
|
||||
import type { TripPlanner } from '../MTripShell'
|
||||
@@ -170,7 +170,7 @@ export default function ImpFileStep({ planner, onBack, onDone }: ImpFileStepProp
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="mt-3 flex min-h-[88px] w-full flex-col items-center justify-center gap-[6px] rounded-[14px] border-[1.5px] border-dashed border-[color:var(--m-trackoff)] p-4"
|
||||
>
|
||||
<Upload size={17} strokeWidth={1.9} className="text-m-faint" />
|
||||
<FileDown size={17} strokeWidth={1.9} className="text-m-faint" />
|
||||
{files.length > 0 ? (
|
||||
<span className="break-all text-center text-[0.78125rem] font-semibold text-m-ink">
|
||||
{files.map(f => f.name).join(', ')}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Download, MapPin, Upload } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Download, FileDown, MapPin } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import MSheet from '../../../components/MSheet'
|
||||
import MIconBtn from '../../../components/MIconBtn'
|
||||
@@ -51,7 +51,7 @@ export default function MImportSheet({ planner, open, onClose }: MImportSheetPro
|
||||
{step === 'menu' && (
|
||||
<div className="px-[14px] pb-[14px] pt-1">
|
||||
<ImpMenuRow
|
||||
icon={Upload}
|
||||
icon={FileDown}
|
||||
title={t('places.importFile')}
|
||||
sub="GPX · KML · KMZ"
|
||||
onClick={() => setStep('file')}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { Upload } from 'lucide-react'
|
||||
import { FileDown } from 'lucide-react'
|
||||
import MSheet from '../../../components/MSheet'
|
||||
import { Eyebrow, FIELD_AREA_CLS, FormSheetFooter, FormSheetHeader } from '../sheets/PlSheetChrome'
|
||||
import { packingApi } from '../../../../api/client'
|
||||
@@ -74,7 +74,7 @@ export default function MPackingImportSheet({ planner, open, onClose }: MPacking
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="mt-2 flex items-center gap-[6px] rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[13px] py-[7px] text-[0.75rem] font-semibold text-m-muted"
|
||||
>
|
||||
<Upload size={12} strokeWidth={2.2} />
|
||||
<FileDown size={12} strokeWidth={2.2} />
|
||||
{t('packing.importCsv')}
|
||||
</button>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import {
|
||||
Briefcase, Check, CheckCheck, ChevronDown, ChevronUp, HandHelping,
|
||||
LayoutTemplate, MoreHorizontal, Package, Pencil, Plus, RotateCcw, Save as SaveIcon, Trash2, Upload, UserPlus, UserRound,
|
||||
Download, LayoutTemplate, MoreHorizontal, Package, Pencil, Plus, RotateCcw, Save as SaveIcon, Trash2, UserPlus, UserRound,
|
||||
} from 'lucide-react'
|
||||
import MDancingTrek from '../../../components/MDancingTrek'
|
||||
import { useAuthStore } from '../../../../store/authStore'
|
||||
@@ -323,7 +323,7 @@ export default function MPackingListTab({ planner }: { planner: TripPlanner }) {
|
||||
{isAdmin && items.length > 0 && (
|
||||
<ActionRow icon={SaveIcon} label={t('packing.saveAsTemplate')} onClick={() => setActionView('save')} />
|
||||
)}
|
||||
<ActionRow icon={Upload} label={t('packing.import')} onClick={() => { setActionsOpen(false); setShowImportSheet(true) }} />
|
||||
<ActionRow icon={Download} label={t('packing.import')} onClick={() => { setActionsOpen(false); setShowImportSheet(true) }} />
|
||||
</>
|
||||
)}
|
||||
{actionView === 'apply' && (
|
||||
|
||||
@@ -43,7 +43,7 @@ import { useRouteCalculation } from '../hooks/useRouteCalculation'
|
||||
import { usePlaceSelection } from '../hooks/usePlaceSelection'
|
||||
import { usePlannerHistory } from '../hooks/usePlannerHistory'
|
||||
import type { Accommodation, TripMember, Day, Place, Reservation, PackingItem, TodoItem } from '../types'
|
||||
import { ListTodo, Upload, Plus, Trash2, FolderPlus } from 'lucide-react'
|
||||
import { ListTodo, Download, Plus, Trash2, FolderPlus } from 'lucide-react'
|
||||
import { useTripPlanner } from './tripPlanner/useTripPlanner'
|
||||
import { usePoiExplore } from '../components/Map/usePoiExplore'
|
||||
import PoiCategoryPill from '../components/Map/PoiCategoryPill'
|
||||
@@ -189,7 +189,7 @@ function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; p
|
||||
className={`${sharedBtnClass} bg-accent text-accent-text`}
|
||||
style={sharedBtnStyle}
|
||||
>
|
||||
<Upload size={14} strokeWidth={2.5} />
|
||||
<Download size={14} strokeWidth={2.5} />
|
||||
<span className="hidden sm:inline">{t('packing.import')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user