diff --git a/client/src/App.css b/client/src/App.css index 0928d4e..7bf84a8 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -3728,10 +3728,315 @@ html.theme-cupertino .events-scroll-container { } .live-log-layout { + display: flex; + flex-direction: column; + gap: 16px; + align-items: stretch; +} + +.live-log-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.live-log-header-main { + display: flex; + align-items: flex-start; + gap: 12px; + min-width: 0; +} + +.live-log-header-main h2 { + margin: 0; + font-size: 1.25rem; +} + +.live-log-overflow-trigger { + flex-shrink: 0; + min-width: 44px; + min-height: 44px; +} + +.live-day-chips { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 8px; + margin-bottom: 12px; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} + +.live-day-chips::-webkit-scrollbar { + display: none; +} + +.live-day-chip { + flex-shrink: 0; + padding: 8px 14px; + min-height: 40px; + border-radius: 999px; + border: 1px solid var(--app-border-muted); + background: var(--app-surface); + color: var(--app-text); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.live-day-chip.is-active { + background: rgba(59, 130, 246, 0.15); + border-color: rgba(59, 130, 246, 0.45); + color: var(--app-accent-light); +} + +.live-day-chip-all { + border-style: dashed; +} + +.live-action-toolbar-sticky { + position: sticky; + top: 0; + z-index: 5; + background: var(--app-bg, transparent); + padding-bottom: 8px; + margin-bottom: 4px; +} + +.live-action-toolbar { + display: flex; + flex-direction: column; + gap: 10px; +} + +.live-action-grid { display: grid; - grid-template-columns: minmax(148px, 200px) 1fr; - gap: 20px; - align-items: start; + gap: 8px; +} + +.live-action-grid-primary { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.live-action-grid-secondary { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.live-log-action-btn-compact { + flex-direction: column; + justify-content: center; + text-align: center; + min-height: 48px; + padding: 10px 8px; + gap: 6px; + font-size: 12px; + line-height: 1.2; +} + +.live-log-action-btn-compact span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.live-log-action-btn-compact.has-gps { + border-color: rgba(34, 197, 94, 0.45); +} + +.live-log-action-btn-compact.has-context { + border-color: rgba(59, 130, 246, 0.35); +} + +.live-log-entry { + display: flex; + align-items: center; + gap: 4px; +} + +.live-log-entry-tap { + flex: 1; + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; + padding: 0; + border: none; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; + border-radius: 8px; +} + +.live-log-entry-tap:hover { + background: rgba(59, 130, 246, 0.06); +} + +.live-log-entry-edit { + flex-shrink: 0; + opacity: 0.6; +} + +.live-log-entry:hover .live-log-entry-edit, +.live-log-entry-edit:focus-visible { + opacity: 1; +} + +.live-bottom-sheet-backdrop { + position: fixed; + inset: 0; + z-index: 10055; + background: rgba(0, 0, 0, 0.45); + display: flex; + align-items: flex-end; + justify-content: center; + padding: 0; +} + +.live-bottom-sheet { + width: 100%; + max-width: 520px; + max-height: min(85vh, 640px); + overflow-y: auto; + background: var(--app-surface-alt); + border-radius: 16px 16px 0 0; + border: 1px solid var(--app-border-muted); + border-bottom: none; + padding: 12px 16px calc(16px + env(safe-area-inset-bottom)); + box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.25); +} + +.live-bottom-sheet-handle { + width: 36px; + height: 4px; + border-radius: 2px; + background: var(--app-border-muted); + margin: 0 auto 12px; +} + +.live-bottom-sheet-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.live-bottom-sheet-header h3 { + margin: 0; + font-size: 16px; +} + +.live-bottom-sheet-actions { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; +} + +.live-bottom-sheet-actions .btn { + width: 100%; + justify-content: center; +} + +.live-event-sheet-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.live-event-sheet-field { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + color: var(--app-text-muted); +} + +.live-overflow-menu-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.live-overflow-menu-item { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 48px; + padding: 12px 14px; + border: none; + border-radius: var(--app-radius-btn, 10px); + background: transparent; + color: var(--app-text); + font-size: 15px; + text-align: left; + cursor: pointer; +} + +.live-overflow-menu-item:hover:not(:disabled) { + background: rgba(59, 130, 246, 0.08); +} + +.live-overflow-menu-item:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.live-overflow-menu-item-danger { + color: #f87171; +} + +.danger-text { + color: #f87171; +} + +@media (min-width: 481px) { + .live-action-grid-primary { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .live-action-grid-secondary { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } +} + +@media (min-width: 769px) { + .live-action-grid-primary { + grid-template-columns: repeat(7, minmax(0, 1fr)); + } + + .live-action-grid-secondary { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + .live-bottom-sheet-backdrop { + align-items: center; + padding: 24px; + } + + .live-bottom-sheet { + border-radius: 16px; + border-bottom: 1px solid var(--app-border-muted); + max-height: min(80vh, 560px); + } + + .live-bottom-sheet-handle { + display: none; + } +} + +@media (max-width: 768px) { + .live-log-undo-bar { + bottom: calc(56px + env(safe-area-inset-bottom, 0px)); + } } .live-log-actions { @@ -4046,16 +4351,12 @@ html.theme-cupertino .events-scroll-container { } @media (max-width: 720px) { - .live-log-layout { - grid-template-columns: 1fr; - } - .live-log-actions { flex-direction: row; flex-wrap: wrap; } - .live-log-action-btn { + .live-log-action-btn:not(.live-log-action-btn-compact) { width: auto; flex: 1 1 calc(50% - 4px); min-width: 140px; diff --git a/client/src/components/LiveActionToolbar.tsx b/client/src/components/LiveActionToolbar.tsx new file mode 100644 index 0000000..e7d773b --- /dev/null +++ b/client/src/components/LiveActionToolbar.tsx @@ -0,0 +1,272 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + Anchor, + Camera, + CloudSun, + Compass, + Droplets, + Fuel, + Gauge, + MapPin, + MessageSquare, + Mic, + MoreHorizontal, + Sailboat, + Waves, + X, + Zap +} from 'lucide-react' + +export interface LiveActionToolbarProps { + motorRunning: boolean + hasLoggedPosition: boolean + lastSailsLabel?: string + busy: boolean + photoSaving: boolean + voiceSaving: boolean + tidesLoading: boolean + weatherOwmLoading: boolean + onMotorToggle: () => void + onCastOff: () => void + onMoor: () => void + onOpenSails: () => void + onOpenCourse: () => void + onOpenSog: () => void + onOpenStw: () => void + onOpenFuel: () => void + onOpenWater: () => void + onOpenPosition: () => void + onFetchTides: () => void + onOpenComment: () => void + onOpenPhoto: () => void + onOpenVoice: () => void + onFetchOwmWeather: () => void + onOpenWind: () => void + onOpenTemp: () => void + onOpenPressure: () => void + onOpenPrecip: () => void + onOpenSeaState: () => void + onOpenVisibility: () => void +} + +export default function LiveActionToolbar({ + motorRunning, + hasLoggedPosition, + lastSailsLabel, + busy, + photoSaving, + voiceSaving, + tidesLoading, + weatherOwmLoading, + onMotorToggle, + onCastOff, + onMoor, + onOpenSails, + onOpenCourse, + onOpenSog, + onOpenStw, + onOpenFuel, + onOpenWater, + onOpenPosition, + onFetchTides, + onOpenComment, + onOpenPhoto, + onOpenVoice, + onFetchOwmWeather, + onOpenWind, + onOpenTemp, + onOpenPressure, + onOpenPrecip, + onOpenSeaState, + onOpenVisibility +}: LiveActionToolbarProps) { + const { t } = useTranslation() + const [weatherSheetOpen, setWeatherSheetOpen] = useState(false) + const [moreSheetOpen, setMoreSheetOpen] = useState(false) + + const closeSheets = () => { + setWeatherSheetOpen(false) + setMoreSheetOpen(false) + } + + const runWeather = (fn: () => void) => { + fn() + closeSheets() + } + + const runMore = (fn: () => void) => { + fn() + closeSheets() + } + + return ( + <> +
{t('logs.travel_day_number', { number: dayOfTravel })} · {new Date(date).toLocaleDateString()} + {departure && destination && ( + <> · {departure} → {destination}> + )} + {departure && !destination && ( + <> · {departure}> + )}
{t('logs.live_no_events')}