diff --git a/client/src/components/Collections/CollectionPicker.tsx b/client/src/components/Collections/CollectionPicker.tsx index bf2a5d81d..1b51e9490 100644 --- a/client/src/components/Collections/CollectionPicker.tsx +++ b/client/src/components/Collections/CollectionPicker.tsx @@ -215,8 +215,8 @@ export default function CollectionPicker({ bias, onSelect, t }: CollectionPicker scene="search" title={t('collections.picker.empty')} size={84} + fill surface="var(--bg-secondary)" - compact /> ) : (
diff --git a/client/src/components/Planner/PlaceDetailsColumn.tsx b/client/src/components/Planner/PlaceDetailsColumn.tsx index db9db194f..c992a98ce 100644 --- a/client/src/components/Planner/PlaceDetailsColumn.tsx +++ b/client/src/components/Planner/PlaceDetailsColumn.tsx @@ -205,7 +205,17 @@ export default function PlaceDetailsColumn({
- {!selection &&

{t('places.details.empty')}

} + {/* Nothing picked yet, which is a state rather than a fault: the mascot + waits with an idle look instead of a sad one. */} + {!selection && ( + + )} {selection && state === 'loading' && (
@@ -231,8 +241,8 @@ export default function PlaceDetailsColumn({ mood="sad" title={t('places.details.nothing')} size={92} + fill surface="var(--bg-secondary)" - compact /> )} diff --git a/client/src/components/Planner/PlacesSidebarList.tsx b/client/src/components/Planner/PlacesSidebarList.tsx index b5acc4d07..1c0f60799 100644 --- a/client/src/components/Planner/PlacesSidebarList.tsx +++ b/client/src/components/Planner/PlacesSidebarList.tsx @@ -1,4 +1,5 @@ import { Fragment } from 'react' +import EmptyState from '../shared/EmptyState' import { MemoPlaceRow } from './PlacesSidebarRow' import type { SidebarState } from './usePlacesSidebar' import { usePluginViewContributions, PluginCardFooter } from '../Plugins/PluginContributions' @@ -14,14 +15,26 @@ export function PlacesList(S: SidebarState) { return (
onScrollTopChange?.((e.currentTarget as HTMLElement).scrollTop)}> {filtered.length === 0 ? ( -
- - {filter === 'unplanned' ? t('places.allPlanned') : t('places.noneFound')} - - {canEditPlaces && } -
+ /* The mascot and one line, the shape every other empty state in TREK has. + The add link stays as the state's action: an empty list of places is + one of the few that has an obvious next step. */ + + {t('places.addPlace')} + + ) : undefined} + /> ) : ( filtered.map(place => { const cat = categories.find(c => c.id === place.category_id) diff --git a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx index 10428dcbd..b4dd2ac26 100644 --- a/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx +++ b/client/src/components/Roadtrip/RoadtripCorridorPanel.tsx @@ -509,7 +509,7 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro scene={search.loading ? 'search' : 'idle'} mood={filteredToNothing ? 'confused' : undefined} size={88} - compact + fill surface="var(--bg-secondary)" title={ search.loading diff --git a/client/src/components/Roadtrip/RoadtripSidebar.tsx b/client/src/components/Roadtrip/RoadtripSidebar.tsx index cc748c624..c68e90026 100644 --- a/client/src/components/Roadtrip/RoadtripSidebar.tsx +++ b/client/src/components/Roadtrip/RoadtripSidebar.tsx @@ -1044,8 +1044,11 @@ export default function RoadtripSidebar({ // Nothing to total up, so nothing pretends to: no "0 km" standing above "No route yet". if (routes.days.length === 0) { + // Centred in the rail rather than sitting at its top: the state is about the + // whole column being empty, and a mascot pinned under the header reads as a + // header decoration. return ( -
+
{/* The mascot rather than a pin and two lines of instructions. The rail is empty because the trip has no days with places yet, which is a state, not a task list: the same look every other empty state in TREK diff --git a/client/src/components/shared/EmptyState.tsx b/client/src/components/shared/EmptyState.tsx index 89745ac79..c48112937 100644 --- a/client/src/components/shared/EmptyState.tsx +++ b/client/src/components/shared/EmptyState.tsx @@ -23,6 +23,7 @@ export default function EmptyState({ surface = 'var(--bg-card)', layout = 'stack', compact = false, + fill = false, className = '', action, }: { @@ -40,13 +41,28 @@ export default function EmptyState({ * about why the list below is blank. */ compact?: boolean + /** + * Fill the parent and sit in the middle of it. + * + * The stack already centres what it contains, but with no height of its own + * `justify-center` has nothing to centre inside, so the state lands under + * whatever sits above it. A state that stands for a whole empty column wants + * the middle of that column; one that sits inline in a page does not, which is + * why this is a choice rather than the default. The parent has to be the one + * with the height, which every scroll container here already is. + * + * Optically centred rather than geometrically: the extra room goes underneath, + * which lands the mascot slightly above the middle. In a column you can scroll, + * dead centre reads as "fell to the bottom" the moment the list is short. + */ + fill?: boolean className?: string /** Optional call to action under the title, for states that have an obvious next step. */ action?: ReactNode }) { const layoutClasses = layout === 'row' ? 'flex flex-row items-center justify-center gap-3 px-6 py-3' - : 'flex flex-col items-center justify-center gap-3 px-6 py-12 text-center' + : `flex flex-col items-center justify-center gap-3 px-6 text-center ${fill ? 'min-h-full pt-4 pb-14' : 'py-12'}` return (