diff --git a/client/src/App.css b/client/src/App.css
index ac62706..75a0b30 100644
--- a/client/src/App.css
+++ b/client/src/App.css
@@ -4241,6 +4241,79 @@ html.theme-cupertino .events-scroll-container {
}
}
+/* Compact weather metric sliders (LogEntryEditor) */
+.weather-metrics-grid {
+ gap: 12px 16px;
+}
+
+.weather-metrics-grid .weather-metrics-span-2 {
+ grid-column: 1 / -1;
+}
+
+.metric-range-input--compact {
+ gap: 0;
+ margin: 0;
+}
+
+.metric-range-input--compact .metric-range-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 6px;
+}
+
+.metric-range-input--compact .metric-range-header label {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.25;
+}
+
+.metric-range-input--compact .metric-range-value {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: #cbd5e1;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.metric-range-input--compact .metric-range-control-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.metric-range-input--compact .metric-range-slider {
+ flex: 1;
+ min-width: 0;
+ width: auto;
+ margin: 0;
+}
+
+.metric-range-input--compact .metric-range-number {
+ width: 4.25rem;
+ min-width: 4.25rem;
+ max-width: 4.25rem;
+ flex-shrink: 0;
+ padding: 8px 6px;
+ text-align: center;
+ font-size: 0.9375rem;
+}
+
+@media (max-width: 480px) {
+ .metric-range-input--compact .metric-range-slider {
+ --tank-slider-thumb: 28px;
+ }
+
+ .metric-range-input--compact .metric-range-number {
+ width: 3.75rem;
+ min-width: 3.75rem;
+ max-width: 3.75rem;
+ padding: 10px 4px;
+ }
+}
+
.vessel-tanks-section {
grid-column: 1 / -1;
margin-top: 8px;
diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx
index d129592..873196d 100644
--- a/client/src/components/LiveLogView.tsx
+++ b/client/src/components/LiveLogView.tsx
@@ -82,6 +82,7 @@ type LiveModal =
| 'temp'
| 'precip'
| 'sea_state'
+ | 'visibility'
| 'course'
| 'fuel'
| 'water'
@@ -557,6 +558,12 @@ export default function LiveLogView({
remarks: LIVE_EVENT_CODES.PRESSURE
})
}
+ if (parsed.visibility) {
+ partials.push({
+ visibility: parsed.visibility,
+ remarks: LIVE_EVENT_CODES.VISIBILITY
+ })
+ }
if (parsed.tempC) {
partials.push({ remarks: liveTempRemark(parsed.tempC) })
}
@@ -724,6 +731,16 @@ export default function LiveLogView({
})
}, 'sea_state')
break
+ case 'visibility':
+ if (!primary) return
+ setModal('none')
+ void runQuickAction(async () => {
+ await appendQuickEvent(logbookId, entryId, {
+ visibility: primary,
+ remarks: LIVE_EVENT_CODES.VISIBILITY
+ })
+ }, 'visibility')
+ break
case 'course': {
const course = primary || lastCourseFromEvents(events)
if (!course) return
@@ -923,6 +940,9 @@ export default function LiveLogView({
+
)}
@@ -1165,7 +1185,7 @@ export default function LiveLogView({
)}
- {['pressure', 'temp', 'precip', 'sea_state', 'fuel', 'water', 'sog', 'stw'].includes(modal) && (
+ {['pressure', 'temp', 'precip', 'sea_state', 'visibility', 'fuel', 'water', 'sog', 'stw'].includes(modal) && (
setModal('none')}>
e.stopPropagation()}>
@@ -1173,6 +1193,7 @@ export default function LiveLogView({
{modal === 'temp' && t('logs.live_temp_btn')}
{modal === 'precip' && t('logs.live_precip_btn')}
{modal === 'sea_state' && t('logs.live_sea_state_btn')}
+ {modal === 'visibility' && t('logs.live_visibility_btn')}
{modal === 'fuel' && t('logs.live_fuel_btn')}
{modal === 'water' && t('logs.live_water_btn')}
{modal === 'sog' && t('logs.live_sog_btn')}
@@ -1192,7 +1213,8 @@ export default function LiveLogView({
: modal === 'temp' ? t('logs.live_temp_placeholder')
: modal === 'precip' ? t('logs.live_precip_placeholder')
: modal === 'sea_state' ? t('logs.live_sea_state_placeholder')
- : modal === 'fuel' ? t('logs.live_fuel_placeholder')
+ : modal === 'visibility' ? t('logs.live_visibility_placeholder')
+ : modal === 'fuel' ? t('logs.live_fuel_placeholder')
: modal === 'water' ? t('logs.live_water_placeholder')
: modal === 'sog' ? t('logs.live_sog_placeholder')
: t('logs.live_stw_placeholder')
diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx
index a7aefe0..dccaa17 100644
--- a/client/src/components/LogEntryEditor.tsx
+++ b/client/src/components/LogEntryEditor.tsx
@@ -56,6 +56,25 @@ import { computeTrackStats, formatTrackStats } from '../utils/trackStats.js'
import { computeFuelPerMotorHour, formatFuelPerMotorHour } from '../utils/fuelStats.js'
import { useRegisterUnsavedChanges } from '../context/UnsavedChangesContext.tsx'
import TankLiterInput from './TankLiterInput.tsx'
+import MetricRangeInput from './MetricRangeInput.tsx'
+import {
+ formatHeelDeg,
+ formatPressureHpa,
+ formatSeaState,
+ formatVisibilityMeters,
+ HEEL_MAX_DEG,
+ HEEL_MIN_DEG,
+ parseHeelDeg,
+ parsePressureHpa,
+ parseSeaState,
+ parseVisibilityMeters,
+ PRESSURE_DEFAULT_HPA,
+ PRESSURE_MAX_HPA,
+ PRESSURE_MIN_HPA,
+ SEA_STATE_MAX,
+ SEA_STATE_MIN,
+ VISIBILITY_STEPS_M
+} from '../utils/weatherMetrics.js'
import {
computeEveningTankMaxLiters,
computeRefilledTankMaxLiters,
@@ -201,6 +220,7 @@ export default function LogEntryEditor({
const [evWindDirection, setEvWindDirection] = useState('')
const [evWindStrength, setEvWindStrength] = useState('')
const [evSeaState, setEvSeaState] = useState('')
+ const [evVisibility, setEvVisibility] = useState('')
const [evWeatherIcon, setEvWeatherIcon] = useState('')
const [evCurrent, setEvCurrent] = useState('')
const [evHeel, setEvHeel] = useState('')
@@ -361,6 +381,7 @@ export default function LogEntryEditor({
windDirection: evWindDirection,
windStrength: evWindStrength,
seaState: evSeaState,
+ visibility: evVisibility,
weatherIcon: evWeatherIcon,
current: evCurrent,
heel: evHeel,
@@ -383,7 +404,7 @@ export default function LogEntryEditor({
return hasUnsavedEventDraft(buildEventFromForm(), editingEventIndex, events)
}, [
evTime, evMgk, evRwk, evWindPressure, evWindDirection, evWindStrength, evSeaState,
- evWeatherIcon, evCurrent, evHeel, evSailsOrMotor, evLogReading, evDistance,
+ evVisibility, evWeatherIcon, evCurrent, evHeel, evSailsOrMotor, evLogReading, evDistance,
evGpsLat, evGpsLng, evRemarks, editingEventIndex, events
])
@@ -985,6 +1006,7 @@ export default function LogEntryEditor({
setEvWindStrength(parsed.windStrength)
setEvWindPressure(parsed.windPressure)
if (parsed.windDirection) setEvWindDirection(parsed.windDirection)
+ if (parsed.visibility) setEvVisibility(parsed.visibility)
if (parsed.weatherIcon) setEvWeatherIcon(parsed.weatherIcon)
showAlert(t('settings.weather_success'))
@@ -1047,6 +1069,7 @@ export default function LogEntryEditor({
setEvWindDirection('')
setEvWindStrength('')
setEvSeaState('')
+ setEvVisibility('')
setEvWeatherIcon('')
setEvCurrent('')
setEvHeel('')
@@ -1070,6 +1093,7 @@ export default function LogEntryEditor({
setEvWindDirection(normalized.windDirection)
setEvWindStrength(normalized.windStrength)
setEvSeaState(normalized.seaState)
+ setEvVisibility(normalized.visibility)
setEvWeatherIcon(normalized.weatherIcon)
setEvCurrent(normalized.current)
setEvHeel(normalized.heel)
@@ -1698,8 +1722,8 @@ export default function LogEntryEditor({
-