feat(mobile): Handy-Optimierung – Tap-to-place, Bottom-Nav, kompakte Banner (v1.4.12)
- Tap-to-place: Symbol antippen → auf Karte tippen platziert es (Alternative zu Drag&Drop, mobil-tauglich); schwebender Hinweis mit Abbrechen - Mobile Bottom-Navigation: Karte / Journal(Auswertung) / Symbole ohne Menü erreichbar - FAB entfernt; Karten-Steuerung (Zoom/Standort/Massstab) hebt sich mobil über die Bottom-Nav - Touch-Ziele in der Werkzeugleiste auf 44px vergrössert - "Einsatz/Übung beenden" jetzt auch im Mobile-Overflow-Menü - Banner (Übung/Offline/E-Mail/Live) mobil kompakt: kleinere Höhe, gekürzte Texte, truncate - Einsatz-beenden-Bestätigung als In-App-Dialog statt Browser-confirm() Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,20 +75,20 @@ export function LeftToolbar({
|
||||
}: LeftToolbarProps) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<aside className="w-11 md:w-14 lg:w-20 border-r border-border bg-card flex flex-col items-center py-1 md:py-2 shrink-0 overflow-y-auto overflow-x-hidden z-10">
|
||||
{/* Draw Tools */}
|
||||
<div className="flex flex-col gap-px md:gap-0.5">
|
||||
<aside className="w-14 lg:w-20 border-r border-border bg-card flex flex-col items-center py-1.5 lg:py-2 shrink-0 overflow-y-auto overflow-x-hidden z-10">
|
||||
{/* Draw Tools — Touch-Ziele mind. 44px auf Mobil/Tablet */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{drawTools.map((tool) => (
|
||||
<Tooltip key={tool.mode}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={drawMode === tool.mode ? 'default' : 'ghost'}
|
||||
size="icon"
|
||||
className="w-8 h-8 md:w-10 md:h-10 lg:w-12 lg:h-12"
|
||||
className="w-11 h-11 lg:w-12 lg:h-12"
|
||||
onClick={() => onDrawModeChange(tool.mode)}
|
||||
disabled={!canEdit && tool.mode !== 'select'}
|
||||
>
|
||||
<tool.icon className="w-4 h-4 md:w-5 md:h-5 lg:w-6 lg:h-6" />
|
||||
<tool.icon className="w-5 h-5 lg:w-6 lg:h-6" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
@@ -98,20 +98,20 @@ export function LeftToolbar({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-6 md:w-8 lg:w-12 h-px bg-border my-1" />
|
||||
<div className="w-8 lg:w-12 h-px bg-border my-1" />
|
||||
|
||||
{/* Undo/Redo - 2-col grid to save vertical space */}
|
||||
<div className="grid grid-cols-2 gap-px md:gap-0.5">
|
||||
{/* Undo/Redo — mobil gestapelt (44px), Desktop 2-spaltig */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-5 h-5 md:w-6 md:h-6 lg:w-8 lg:h-8"
|
||||
className="w-11 h-11 lg:w-8 lg:h-8"
|
||||
onClick={onUndo}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<Undo2 className="w-3.5 h-3.5 md:w-4 md:h-4 lg:w-5 lg:h-5" />
|
||||
<Undo2 className="w-5 h-5 lg:w-5 lg:h-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
@@ -124,11 +124,11 @@ export function LeftToolbar({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-5 h-5 md:w-6 md:h-6 lg:w-8 lg:h-8"
|
||||
className="w-11 h-11 lg:w-8 lg:h-8"
|
||||
onClick={onRedo}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<Redo2 className="w-3.5 h-3.5 md:w-4 md:h-4 lg:w-5 lg:h-5" />
|
||||
<Redo2 className="w-5 h-5 lg:w-5 lg:h-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
@@ -138,15 +138,15 @@ export function LeftToolbar({
|
||||
|
||||
</div>
|
||||
|
||||
<div className="w-6 md:w-8 lg:w-12 h-px bg-border my-1" />
|
||||
<div className="w-8 lg:w-12 h-px bg-border my-1" />
|
||||
|
||||
{/* Color Picker - 2-col grid */}
|
||||
<div className="grid grid-cols-2 gap-0.5 md:gap-1">
|
||||
{/* Color Picker - 2-col grid, grössere Swatches für Touch */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{colors.map((color) => (
|
||||
<button
|
||||
key={color.value}
|
||||
className={cn(
|
||||
'w-4 h-4 md:w-5 md:h-5 lg:w-7 lg:h-7 rounded border-2 transition-all',
|
||||
'w-6 h-6 lg:w-7 lg:h-7 rounded border-2 transition-all',
|
||||
selectedColor === color.value
|
||||
? 'border-primary ring-1 ring-primary ring-offset-1 scale-110'
|
||||
: 'border-muted hover:border-muted-foreground'
|
||||
|
||||
@@ -31,6 +31,7 @@ interface TenantSymbolGroup {
|
||||
|
||||
interface RightSidebarProps {
|
||||
onSymbolDrop: (iconId: string, coordinates: [number, number], imageUrl?: string) => void
|
||||
onSymbolSelect?: (symbol: { id: string; imageUrl: string }) => void
|
||||
canEdit: boolean
|
||||
isOpen?: boolean
|
||||
onToggle?: () => void
|
||||
@@ -56,9 +57,10 @@ const categoryIcons: Record<string, typeof Flame> = {
|
||||
'Eigene': Upload,
|
||||
}
|
||||
|
||||
function DraggableSymbol({ symbol, canEdit }: {
|
||||
function DraggableSymbol({ symbol, canEdit, onSelect }: {
|
||||
symbol: DisplaySymbol
|
||||
canEdit: boolean
|
||||
onSelect?: (symbol: DisplaySymbol) => void
|
||||
}) {
|
||||
const [{ isDragging }, drag, preview] = useDrag(() => ({
|
||||
type: 'SYMBOL',
|
||||
@@ -77,6 +79,7 @@ function DraggableSymbol({ symbol, canEdit }: {
|
||||
return (
|
||||
<div
|
||||
ref={drag as unknown as React.LegacyRef<HTMLDivElement>}
|
||||
onClick={() => { if (canEdit && onSelect) onSelect(symbol) }}
|
||||
className={`
|
||||
flex flex-col items-center gap-1 p-1.5 md:p-2 lg:p-2.5 rounded-lg border-2 transition-all
|
||||
border-transparent hover:border-border hover:bg-accent
|
||||
@@ -84,7 +87,7 @@ function DraggableSymbol({ symbol, canEdit }: {
|
||||
${isDragging ? 'opacity-0' : ''}
|
||||
${!canEdit ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
title={symbol.name}
|
||||
title={canEdit ? `${symbol.name} — antippen zum Platzieren, oder auf die Karte ziehen` : symbol.name}
|
||||
>
|
||||
<div className="w-10 h-10 md:w-11 md:h-11 lg:w-14 lg:h-14 flex items-center justify-center rounded-lg bg-white border border-gray-200 dark:border-gray-600">
|
||||
<img
|
||||
@@ -101,7 +104,7 @@ function DraggableSymbol({ symbol, canEdit }: {
|
||||
)
|
||||
}
|
||||
|
||||
export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTab, onTabChange, isCollapsed, onToggleCollapse, tenantId, mode }: RightSidebarProps) {
|
||||
export function RightSidebar({ onSymbolDrop, onSymbolSelect, canEdit, isOpen, onToggle, activeTab, onTabChange, isCollapsed, onToggleCollapse, tenantId, mode }: RightSidebarProps) {
|
||||
const journalLabel = mode === 'UEBUNG' ? 'Auswertung' : 'Journal'
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [activeCategory, setActiveCategory] = useState<string>('')
|
||||
@@ -206,17 +209,6 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile toggle button */}
|
||||
{!isOpen && onToggle && (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="md:hidden fixed bottom-4 right-4 z-50 w-14 h-14 bg-primary text-primary-foreground rounded-full shadow-lg flex items-center justify-center active:scale-95 transition-transform"
|
||||
title="Symbole"
|
||||
>
|
||||
<LayoutGrid className="w-6 h-6" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Backdrop on mobile */}
|
||||
{isOpen && onToggle && (
|
||||
<div className="md:hidden fixed inset-0 bg-black/40 z-40" onClick={onToggle} />
|
||||
@@ -375,7 +367,7 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
<div className="px-1.5 pb-1.5">
|
||||
<div className="grid grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-1">
|
||||
{g.symbols.map(symbol => (
|
||||
<DraggableSymbol key={symbol.id} symbol={symbol} canEdit={canEdit} />
|
||||
<DraggableSymbol key={symbol.id} symbol={symbol} canEdit={canEdit} onSelect={onSymbolSelect} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -440,7 +432,7 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
) : (
|
||||
<div className="grid grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-1">
|
||||
{currentCategory.symbols.map((symbol) => (
|
||||
<DraggableSymbol key={symbol.id} symbol={symbol} canEdit={canEdit} />
|
||||
<DraggableSymbol key={symbol.id} symbol={symbol} canEdit={canEdit} onSelect={onSymbolSelect} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -355,6 +355,12 @@ export function Topbar({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
{project && onEndProject && (
|
||||
<DropdownMenuItem onClick={onEndProject} className="text-red-600 focus:text-red-700">
|
||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||
{project.mode === 'UEBUNG' ? 'Übung beenden' : 'Einsatz beenden'}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={onToggleTheme}>
|
||||
{isDarkMode ? <Sun className="w-4 h-4 mr-2" /> : <Moon className="w-4 h-4 mr-2" />}
|
||||
{isDarkMode ? 'Tagmodus' : 'Nachtmodus'}
|
||||
|
||||
@@ -85,6 +85,9 @@ interface MapViewProps {
|
||||
mapRef?: React.MutableRefObject<maplibregl.Map | null>
|
||||
onDrawModeChange?: (mode: DrawMode) => void
|
||||
undoDrawPointRef?: React.MutableRefObject<(() => boolean) | null>
|
||||
/** Tap-to-place: wenn gesetzt, platziert der nächste Karten-Tipp dieses Symbol (Mobil-freundlich). */
|
||||
pendingSymbol?: { iconId: string; imageUrl?: string } | null
|
||||
onSymbolPlaced?: () => void
|
||||
}
|
||||
|
||||
export function MapView({
|
||||
@@ -100,6 +103,8 @@ export function MapView({
|
||||
mapRef: externalMapRef,
|
||||
onDrawModeChange,
|
||||
undoDrawPointRef,
|
||||
pendingSymbol,
|
||||
onSymbolPlaced,
|
||||
}: MapViewProps) {
|
||||
const mapContainer = useRef<HTMLDivElement | null>(null)
|
||||
const map = useRef<maplibregl.Map | null>(null)
|
||||
@@ -165,6 +170,8 @@ export function MapView({
|
||||
const onSymbolDropRef = useRef(onSymbolDrop)
|
||||
const onTextPlaceRef = useRef(onTextPlace)
|
||||
const onDrawModeChangeRef = useRef(onDrawModeChange)
|
||||
const pendingSymbolRef = useRef(pendingSymbol)
|
||||
const onSymbolPlacedRef = useRef(onSymbolPlaced)
|
||||
|
||||
// Lock/unlock map panning based on draw mode
|
||||
// Apple-style: touchZoomRotate (two-finger pinch/zoom) ALWAYS enabled
|
||||
@@ -191,6 +198,8 @@ export function MapView({
|
||||
useEffect(() => { onSymbolDropRef.current = onSymbolDrop }, [onSymbolDrop])
|
||||
useEffect(() => { onTextPlaceRef.current = onTextPlace }, [onTextPlace])
|
||||
useEffect(() => { onDrawModeChangeRef.current = onDrawModeChange }, [onDrawModeChange])
|
||||
useEffect(() => { pendingSymbolRef.current = pendingSymbol }, [pendingSymbol])
|
||||
useEffect(() => { onSymbolPlacedRef.current = onSymbolPlaced }, [onSymbolPlaced])
|
||||
|
||||
// Expose undo-draw-point function to parent via ref
|
||||
useEffect(() => {
|
||||
@@ -891,6 +900,15 @@ export function MapView({
|
||||
const width = selectedWidthRef.current
|
||||
const coords: [number, number] = [e.lngLat.lng, e.lngLat.lat]
|
||||
|
||||
// Tap-to-place: ein zuvor angetipptes Symbol wird hier platziert (Mobil-freundlich,
|
||||
// Alternative zum Drag&Drop). Hat Vorrang vor allen Zeichen-Modi.
|
||||
if (pendingSymbolRef.current) {
|
||||
const p = pendingSymbolRef.current
|
||||
onSymbolDropRef.current(p.iconId, coords, p.imageUrl)
|
||||
onSymbolPlacedRef.current?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'select') {
|
||||
// Detect click near a line/polygon for vertex editing
|
||||
const pixel = e.point
|
||||
|
||||
Reference in New Issue
Block a user