UI changes
This commit is contained in:
parent
0aba73a2a3
commit
8616837c01
13 changed files with 126 additions and 100 deletions
|
|
@ -130,12 +130,7 @@ export default function FeatureBrowser({
|
|||
className="flex items-center justify-between px-3 py-1.5 hover:bg-teal-50 dark:hover:bg-teal-900/30 dark:text-warm-300"
|
||||
>
|
||||
<div className="min-w-0 mr-2">
|
||||
<FeatureLabel feature={f} size="sm" />
|
||||
{f.description && (
|
||||
<span className="text-xs text-warm-400 dark:text-warm-500 truncate block">
|
||||
{f.description}
|
||||
</span>
|
||||
)}
|
||||
<FeatureLabel feature={f} size="sm" description={f.description} />
|
||||
</div>
|
||||
<FeatureActions
|
||||
feature={f}
|
||||
|
|
@ -174,15 +169,16 @@ export default function FeatureBrowser({
|
|||
<IconButton
|
||||
onClick={() => setTravelInfoMode(mode)}
|
||||
title="Feature info"
|
||||
size="md"
|
||||
>
|
||||
<InfoIcon className="w-3.5 h-3.5" />
|
||||
<InfoIcon className="w-5 h-5 md:w-3.5 md:h-3.5" />
|
||||
</IconButton>
|
||||
<button
|
||||
onClick={() => onAddTravelTimeEntry(mode)}
|
||||
title={`Add ${MODE_LABELS[mode]} travel time`}
|
||||
className="p-1 rounded-md text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/30 hover:bg-teal-100 dark:hover:bg-teal-800/40"
|
||||
>
|
||||
<PlusIcon className="w-7 h-7 md:w-5 md:h-5" strokeWidth={2.5} />
|
||||
<PlusIcon className="w-5 h-5 md:w-5 md:h-5" strokeWidth={2.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -112,8 +112,8 @@ function SliderLabels({
|
|||
onValueChange?: (v: [number, number]) => void;
|
||||
}) {
|
||||
const range = max - min || 1;
|
||||
const leftPct = ((value[0] - min) / range) * 100;
|
||||
const rightPct = ((value[1] - min) / range) * 100;
|
||||
const leftPct = Math.max(0, Math.min(100, ((value[0] - min) / range) * 100));
|
||||
const rightPct = Math.max(0, Math.min(100, ((value[1] - min) / range) * 100));
|
||||
const labels = displayValues || value;
|
||||
|
||||
const minLabel = isAtMin ? 'min' : formatFilterValue(labels[0], raw);
|
||||
|
|
@ -133,7 +133,7 @@ function SliderLabels({
|
|||
<EditableLabel
|
||||
value={labels[0]}
|
||||
formatted={minLabel}
|
||||
onCommit={(v) => onValueChange([Math.min(v, labels[1]), labels[1]])}
|
||||
onCommit={(v) => onValueChange([v, Math.max(v, labels[1])])}
|
||||
prefix={feature.prefix}
|
||||
suffix={feature.suffix}
|
||||
style={{ left: `${leftPct}%`, transform: leftTranslate }}
|
||||
|
|
@ -141,7 +141,7 @@ function SliderLabels({
|
|||
<EditableLabel
|
||||
value={labels[1]}
|
||||
formatted={maxLabel}
|
||||
onCommit={(v) => onValueChange([labels[0], Math.max(v, labels[0])])}
|
||||
onCommit={(v) => onValueChange([Math.min(labels[0], v), v])}
|
||||
prefix={feature.prefix}
|
||||
suffix={feature.suffix}
|
||||
style={{ left: `${rightPct}%`, transform: rightTranslate }}
|
||||
|
|
@ -184,6 +184,7 @@ interface FiltersProps {
|
|||
onTravelTimeRemoveEntry: (index: number) => void;
|
||||
onTravelTimeSetDestination: (index: number, slug: string, label: string) => void;
|
||||
onTravelTimeRangeChange: (index: number, range: [number, number]) => void;
|
||||
onTravelTimeDragEnd: (index: number) => void;
|
||||
onTravelTimeToggleBest: (index: number) => void;
|
||||
aiFilterLoading: boolean;
|
||||
aiFilterError: string | null;
|
||||
|
|
@ -220,6 +221,7 @@ export default memo(function Filters({
|
|||
onTravelTimeRemoveEntry,
|
||||
onTravelTimeSetDestination,
|
||||
onTravelTimeRangeChange,
|
||||
onTravelTimeDragEnd,
|
||||
onTravelTimeToggleBest,
|
||||
aiFilterLoading,
|
||||
aiFilterError,
|
||||
|
|
@ -415,7 +417,7 @@ export default memo(function Filters({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={scrollRef} className="md:flex-1 md:overflow-y-auto">
|
||||
<div ref={scrollRef} className="md:flex-1 md:overflow-y-auto overflow-x-hidden">
|
||||
<AiFilterInput
|
||||
loading={aiFilterLoading}
|
||||
error={aiFilterError}
|
||||
|
|
@ -470,9 +472,14 @@ export default memo(function Filters({
|
|||
timeRange={entry.timeRange}
|
||||
useBest={entry.useBest}
|
||||
isPinned={pinnedFeature === travelFieldKey(entry)}
|
||||
isActive={activeFeature === travelFieldKey(entry)}
|
||||
dragValue={activeFeature === travelFieldKey(entry) ? dragValue : null}
|
||||
onTogglePin={() => onTogglePin(travelFieldKey(entry))}
|
||||
onSetDestination={(slug, label) => onTravelTimeSetDestination(index, slug, label)}
|
||||
onTimeRangeChange={(range) => onTravelTimeRangeChange(index, range)}
|
||||
onDragStart={() => onDragStart(travelFieldKey(entry))}
|
||||
onDragChange={onDragChange}
|
||||
onDragEnd={() => onTravelTimeDragEnd(index)}
|
||||
onToggleBest={() => onTravelTimeToggleBest(index)}
|
||||
onRemove={() => onTravelTimeRemoveEntry(index)}
|
||||
/>
|
||||
|
|
@ -592,7 +599,7 @@ export default memo(function Filters({
|
|||
min={scale ? 0 : feature.min!}
|
||||
max={scale ? 100 : feature.max!}
|
||||
value={sliderValue}
|
||||
displayValues={scale ? displayValue : undefined}
|
||||
displayValues={displayValue}
|
||||
isAtMin={isAtMin}
|
||||
isAtMax={isAtMax}
|
||||
raw={feature.raw}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useRef, useEffect, useState, useMemo, memo } from 'react';
|
||||
import { Map as MapGL, useControl } from 'react-map-gl/maplibre';
|
||||
import { Map as MapGL, useControl, ScaleControl } from 'react-map-gl/maplibre';
|
||||
import { MapboxOverlay } from '@deck.gl/mapbox';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import type {
|
||||
|
|
@ -212,6 +212,9 @@ export default memo(function Map({
|
|||
maxBounds={MAP_BOUNDS}
|
||||
>
|
||||
<DeckOverlay layers={layers} getTooltip={null} />
|
||||
{!screenshotMode && (
|
||||
<ScaleControl position="bottom-left" maxWidth={100} unit="metric" />
|
||||
)}
|
||||
</MapGL>
|
||||
{screenshotMode ? (
|
||||
ogMode ? (
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ export default function MapPage({
|
|||
const [selectedPOICategories, setSelectedPOICategories] =
|
||||
useState<Set<string>>(initialPOICategories);
|
||||
|
||||
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 600, 'left');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 500, 'right');
|
||||
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 0.45, 'left');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 0.45, 'right');
|
||||
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||
const [poiPaneOpen, setPoiPaneOpen] = useState(false);
|
||||
|
|
@ -141,6 +141,7 @@ export default function MapPage({
|
|||
handleDragStart,
|
||||
handleDragChange,
|
||||
handleDragEnd,
|
||||
handleDragEndNoCommit,
|
||||
handleTogglePin,
|
||||
handleSetPin,
|
||||
handleCancelPin,
|
||||
|
|
@ -204,12 +205,8 @@ export default function MapPage({
|
|||
const handleTravelTimeSetDestination = useCallback(
|
||||
(index: number, slug: string, label: string) => {
|
||||
travelTime.handleSetDestination(index, slug, label);
|
||||
const entry = travelTime.entries[index];
|
||||
if (entry) {
|
||||
handleSetPin(`tt_${entry.mode}_${slug}`);
|
||||
}
|
||||
},
|
||||
[travelTime.handleSetDestination, travelTime.entries, handleSetPin]
|
||||
[travelTime.handleSetDestination]
|
||||
);
|
||||
|
||||
const handleTravelTimeRemoveEntry = useCallback(
|
||||
|
|
@ -223,6 +220,14 @@ export default function MapPage({
|
|||
[travelTime.handleRemoveEntry, travelTime.entries, pinnedFeature, handleCancelPin]
|
||||
);
|
||||
|
||||
const handleTravelTimeDragEnd = useCallback(
|
||||
(index: number) => {
|
||||
const dv = handleDragEndNoCommit();
|
||||
if (dv) travelTime.handleTimeRangeChange(index, dv);
|
||||
},
|
||||
[handleDragEndNoCommit, travelTime.handleTimeRangeChange]
|
||||
);
|
||||
|
||||
const license = useLicense();
|
||||
|
||||
const mapFlyToRef = useRef<((lat: number, lng: number, zoom: number) => void) | null>(null);
|
||||
|
|
@ -571,6 +576,7 @@ export default function MapPage({
|
|||
onTravelTimeRemoveEntry={handleTravelTimeRemoveEntry}
|
||||
onTravelTimeSetDestination={handleTravelTimeSetDestination}
|
||||
onTravelTimeRangeChange={travelTime.handleTimeRangeChange}
|
||||
onTravelTimeDragEnd={handleTravelTimeDragEnd}
|
||||
onTravelTimeToggleBest={travelTime.handleToggleBest}
|
||||
aiFilterLoading={aiFilters.loading}
|
||||
aiFilterError={aiFilters.error}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,14 @@ interface TravelTimeCardProps {
|
|||
timeRange: [number, number] | null;
|
||||
useBest: boolean;
|
||||
isPinned: boolean;
|
||||
isActive: boolean;
|
||||
dragValue: [number, number] | null;
|
||||
onTogglePin: () => void;
|
||||
onSetDestination: (slug: string, label: string) => void;
|
||||
onTimeRangeChange: (range: [number, number]) => void;
|
||||
onDragStart: () => void;
|
||||
onDragChange: (value: [number, number]) => void;
|
||||
onDragEnd: () => void;
|
||||
onToggleBest: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
|
@ -33,9 +38,14 @@ export function TravelTimeCard({
|
|||
timeRange,
|
||||
useBest,
|
||||
isPinned,
|
||||
isActive,
|
||||
dragValue,
|
||||
onTogglePin,
|
||||
onSetDestination,
|
||||
onTimeRangeChange,
|
||||
onDragStart,
|
||||
onDragChange,
|
||||
onDragEnd,
|
||||
onToggleBest,
|
||||
onRemove,
|
||||
}: TravelTimeCardProps) {
|
||||
|
|
@ -52,13 +62,13 @@ export function TravelTimeCard({
|
|||
|
||||
const sliderMin = 0;
|
||||
const sliderMax = 120;
|
||||
const displayRange = timeRange ?? [sliderMin, sliderMax];
|
||||
const displayRange = isActive && dragValue ? dragValue : (timeRange ?? [sliderMin, sliderMax]);
|
||||
|
||||
const ModeIcon = MODE_ICONS[mode];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`space-y-2 px-2 py-2 rounded ${isPinned ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
className={`space-y-2 px-2 py-2 rounded ${isActive ? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30' : isPinned ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -130,7 +140,9 @@ export function TravelTimeCard({
|
|||
max={sliderMax}
|
||||
step={1}
|
||||
value={[displayRange[0], displayRange[1]]}
|
||||
onValueChange={([min, max]) => onTimeRangeChange([min, max])}
|
||||
onValueChange={([min, max]) => onDragChange([min, max])}
|
||||
onPointerDown={() => onDragStart()}
|
||||
onPointerUp={() => onDragEnd()}
|
||||
/>
|
||||
<div className="relative h-4 mt-1 mx-2.5 text-[10px] text-warm-500 dark:text-warm-400 leading-tight">
|
||||
<span className="absolute left-0">{formatFilterValue(displayRange[0])} min</span>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function FeatureActions({
|
|||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
{feature.detail && onShowInfo && (
|
||||
<IconButton onClick={() => onShowInfo(feature)} title="Feature info" size="md">
|
||||
<InfoIcon className="w-7 h-7 md:w-3.5 md:h-3.5" />
|
||||
<InfoIcon className="w-5 h-5 md:w-3.5 md:h-3.5" />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
|
|
@ -32,7 +32,7 @@ export function FeatureActions({
|
|||
active={isPinned}
|
||||
size="md"
|
||||
>
|
||||
<EyeIcon filled={isPinned} className="w-7 h-7 md:w-3.5 md:h-3.5" />
|
||||
<EyeIcon filled={isPinned} className="w-5 h-5 md:w-3.5 md:h-3.5" />
|
||||
</IconButton>
|
||||
{onAdd && (
|
||||
<button
|
||||
|
|
@ -40,7 +40,7 @@ export function FeatureActions({
|
|||
title="Add filter"
|
||||
className="p-1 rounded-md text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/30 hover:bg-teal-100 dark:hover:bg-teal-800/40"
|
||||
>
|
||||
<PlusIcon className="w-7 h-7 md:w-5 md:h-5" strokeWidth={2.5} />
|
||||
<PlusIcon className="w-5 h-5 md:w-5 md:h-5" strokeWidth={2.5} />
|
||||
</button>
|
||||
)}
|
||||
{onRemove && (
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ interface FeatureLabelProps {
|
|||
onShowInfo?: (feature: FeatureMeta) => void;
|
||||
className?: string;
|
||||
size?: 'xs' | 'sm';
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function FeatureLabel({
|
||||
|
|
@ -21,6 +22,7 @@ export function FeatureLabel({
|
|||
onShowInfo,
|
||||
className = '',
|
||||
size = 'xs',
|
||||
description,
|
||||
}: FeatureLabelProps) {
|
||||
const textClass = size === 'sm' ? 'text-sm' : 'text-xs';
|
||||
const iconClass = 'w-3.5 h-3.5 text-teal-600 dark:text-teal-400 shrink-0';
|
||||
|
|
@ -31,12 +33,8 @@ export function FeatureLabel({
|
|||
? feature.modes.map((m) => MODE_LABELS[m] || m).join(' · ')
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${size === 'xs' ? 'items-center' : 'items-start'} gap-1 min-w-0 ${className}`}
|
||||
>
|
||||
{featureIcon}
|
||||
{GroupIcon && <GroupIcon className={iconClass} />}
|
||||
const nameContent = (
|
||||
<>
|
||||
<span
|
||||
className={`${textClass} ${size === 'sm' ? 'font-medium text-navy-950 dark:text-warm-100' : 'text-warm-700 dark:text-warm-300 truncate'}`}
|
||||
>
|
||||
|
|
@ -56,6 +54,23 @@ export function FeatureLabel({
|
|||
<InfoIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${size === 'xs' ? 'items-center' : 'items-start'} gap-1 min-w-0 ${className}`}
|
||||
>
|
||||
{featureIcon}
|
||||
{GroupIcon && <GroupIcon className={iconClass} />}
|
||||
{description ? (
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1">{nameContent}</div>
|
||||
<span className="text-xs text-warm-400 dark:text-warm-500 block">{description}</span>
|
||||
</div>
|
||||
) : (
|
||||
nameContent
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue