Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a9d26c1
WIP: start trip editor mobile bottom drawer 312 (checkpoint)
stef-k May 19, 2026
68a116f
WIP: implement mobile bottom drawer shell (checkpoint)
stef-k May 19, 2026
c423c14
WIP: stabilize mobile drawer evidence coverage (checkpoint)
stef-k May 19, 2026
c89d800
WIP: split mobile drawer styles for LOC check (checkpoint)
stef-k May 19, 2026
fbdeb96
WIP: stabilize mobile drawer validation coverage (checkpoint)
stef-k May 19, 2026
df6d771
WIP: harden trip editor reorder and feedback tests (checkpoint)
stef-k May 19, 2026
591b876
WIP: stabilize mobile drawer editor ownership 312 (checkpoint)
stef-k May 19, 2026
992244b
WIP: restore desktop sidebar search placement 312 (checkpoint)
stef-k May 19, 2026
7eea2fa
WIP: extend visual polish tablet validation budget 312 (checkpoint)
stef-k May 19, 2026
db60d33
WIP: stabilize visual and visit validation under full suite 312 (chec…
stef-k May 19, 2026
0378056
WIP: ignore Playwright artifacts in Vite watcher 312 (checkpoint)
stef-k May 19, 2026
1c02aac
WIP: retry tablet visual map-work activation 312 (checkpoint)
stef-k May 19, 2026
9a5716c
WIP: stabilize visual navigation guard setup 312 (checkpoint)
stef-k May 19, 2026
dcf9660
WIP: extend visual evidence full-suite budget 312 (checkpoint)
stef-k May 19, 2026
94d32cc
WIP: fix metadata drawer ownership blockers 312 (checkpoint)
stef-k May 19, 2026
0d65f90
WIP: stabilize visual map click evidence 312 (checkpoint)
stef-k May 19, 2026
3edf02f
WIP: fix mobile drawer state and tab guard 312 (checkpoint; tests pen…
stef-k May 19, 2026
6a608c8
WIP: harden mobile drawer tab selectors 312 (checkpoint)
stef-k May 19, 2026
aa450ea
WIP: harden visual map-work evidence 312 (checkpoint)
stef-k May 19, 2026
502c1cc
WIP: guard mobile selected-place routing dirty state (checkpoint; tes…
stef-k May 19, 2026
e7246b6
WIP: fix same-tab mobile selection guard (checkpoint)
stef-k May 19, 2026
7d3a417
WIP: restore visual polish timeout budget (checkpoint)
stef-k May 19, 2026
889fe71
WIP: add mobile trip share progress summary (checkpoint)
stef-k May 19, 2026
33d1df6
WIP: harden mobile drawer coverage (checkpoint)
stef-k May 19, 2026
d5ad88f
WIP: start issue 314 drawer polish (checkpoint)
stef-k May 20, 2026
6185838
WIP: fix issue 314 drawer shell consistency (checkpoint)
stef-k May 20, 2026
baf9cd3
WIP: contain mobile drawer selector panels (checkpoint)
stef-k May 20, 2026
53707cc
WIP: start issue 314 resize pass (checkpoint)
stef-k May 20, 2026
f68c909
WIP: handle clean metadata mobile resize (checkpoint)
stef-k May 20, 2026
ddf480f
WIP: pre-validation fix baseline (checkpoint)
stef-k May 20, 2026
c3c0adf
WIP: fix trip editor validation paths (checkpoint)
stef-k May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion ClientApps/trip-editor/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const isLoading = ref(true);
const hasRegionDraftChanges = ref(false);
const workspaceElement = ref<HTMLElement | null>(null);
const mapElement = ref<HTMLElement | null>(null);
const mobileDrawerActive = ref(false);
const navigationStatus = ref<string | null>(null);
const hiddenSegmentIds = ref<Set<string>>(new Set());
const selectedPlaceId = ref<Guid | null>(null);
Expand All @@ -34,6 +35,7 @@ const pendingSearchAdd = ref<{ result: EditorGeocodeSearchResult; regionId: Guid
const completedSearchAddRequestId = ref<number | null>(null);
const editorSurface = useEditorSurface();
let mapAdapter: ReturnType<typeof createTripEditorMap> | null = null;
let mobileDrawerQuery: MediaQueryList | null = null;
const coordinatePicker = {
applyPlaceDraftCoordinate: (placeId: Guid, coordinate: EditorCoordinate): void => mapAdapter?.applyPlaceDraftCoordinate(placeId, coordinate),
startCoordinatePick: (options: CoordinatePickOptions): (() => void) => mapAdapter?.startCoordinatePick(options) ?? (() => undefined)
Expand Down Expand Up @@ -112,6 +114,9 @@ watch(

onMounted(async () => {
setConfirmDialogFocusFallback(workspaceElement.value);
mobileDrawerQuery = window.matchMedia('(max-width: 640px)');
mobileDrawerActive.value = mobileDrawerQuery.matches;
mobileDrawerQuery.addEventListener('change', updateMobileDrawerState);

try {
const loadedState = await loadEditorState(props.config.editorEndpoint);
Expand All @@ -135,9 +140,15 @@ onMounted(async () => {
onUnmounted(() => {
disposeConfirmDialogHost();
setConfirmDialogFocusFallback(null);
mobileDrawerQuery?.removeEventListener('change', updateMobileDrawerState);
mapAdapter?.dispose();
});

/// Keeps the release-scoped phone drawer below tablet/intermediate widths.
function updateMobileDrawerState(event: MediaQueryListEvent): void {
mobileDrawerActive.value = event.matches;
}

const applyMetadata = (metadata: EditorTripMetadata): void => {
if (!state.value) {
return;
Expand Down Expand Up @@ -190,6 +201,10 @@ async function closeActiveEditorBeforeSelection(placeId: Guid): Promise<boolean>
return true;
}

if (mobileDrawerActive.value && tabForTargetKind(target.kind) !== 'regions') {
return await editorSurface.closeActiveTarget(`Discard unsaved ${targetKindLabel(target.kind)} changes before switching tabs?`);
}

if (target.kind !== 'place') {
return true;
}
Expand All @@ -201,6 +216,28 @@ async function closeActiveEditorBeforeSelection(placeId: Guid): Promise<boolean>
return await editorSurface.closeActiveTarget('Discard unsaved place changes before selecting another place?');
}

/// Maps active editor ownership to the phone drawer tab that can keep it visible.
function tabForTargetKind(kind: string): 'trip' | 'regions' | 'segments' {
if (kind === 'segment') {
return 'segments';
}

if (kind === 'region' || kind === 'place' || kind === 'area') {
return 'regions';
}

return 'trip';
}

/// Keeps dirty-discard copy aligned with the manual phone tab switch guard.
function targetKindLabel(kind: string): string {
if (kind === 'metadata') {
return 'trip';
}

return kind;
}

/// Clears selected-place context, closing the selected place editor first when that editor owns the selection.
const clearSelectedPlace = async (): Promise<boolean> => {
if (!state.value || !selectedPlaceId.value) {
Expand Down Expand Up @@ -477,6 +514,9 @@ function focusStatusText(result: FocusActiveEntityResult, target: { kind: string
:hidden-segment-ids="hiddenSegmentIds"
:selected-place-id="selectedPlaceId"
:pending-search-add="pendingSearchAdd"
:mobile-drawer-active="mobileDrawerActive"
:is-map-work-active="isMapWorkActive"
:completed-search-add-request-id="completedSearchAddRequestId"
:coordinate-picker="coordinatePicker"
:polygon-editor="polygonEditor"
:route-editor="routeEditor"
Expand All @@ -488,6 +528,9 @@ function focusStatusText(result: FocusActiveEntityResult, target: { kind: string
:select-place="placeId => selectPlace(placeId, { focusMap: true })"
:clear-selected-place="clearSelectedPlace"
@search-add-opened="handleSearchAddOpened"
@search-add-place="requestSearchAddPlace"
@search-clear-preview="clearSearchPreview"
@search-preview="previewSearchResult"
/>
<main class="trip-editor-map-shell">
<header class="trip-editor-toolbar">
Expand All @@ -509,7 +552,7 @@ function focusStatusText(result: FocusActiveEntityResult, target: { kind: string
</header>
<MapWorkToolbar :controller="editorSurface" />
<MapSearchControl
v-if="!isMapWorkActive"
v-if="!isMapWorkActive && !mobileDrawerActive"
:active-target="activeEditorTarget"
:completed-add-request-id="completedSearchAddRequestId"
:editor-endpoint="props.config.editorEndpoint"
Expand Down
28 changes: 27 additions & 1 deletion ClientApps/trip-editor/src/components/MetadataEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const props = defineProps<{
antiforgeryToken: string;
tripIndexUrl: string;
hasRegionDraftChanges: boolean;
autoOpen?: boolean;
}>();

const emit = defineEmits<{
Expand Down Expand Up @@ -71,6 +72,15 @@ const visibleShareProgressEnabled = computed({
}
});
const progressUrl = computed(() => (!shareProgressUnavailable.value && draft.shareProgressEnabled ? props.metadata.progressPublicUrl : null));
/// Reports persisted share-progress state for read-only Trip summaries.
const shareProgressSummary = computed(() => {
if (!props.metadata.isPublic) {
return 'Unavailable until trip is public';
}

return props.metadata.shareProgressEnabled ? 'Enabled' : 'Disabled';
});
const summaryProgressUrl = computed(() => (props.metadata.isPublic && props.metadata.shareProgressEnabled ? props.metadata.progressPublicUrl : null));
const target = computed<EditorTarget>(() => ({
key: 'metadata',
identity: 'metadata',
Expand Down Expand Up @@ -123,13 +133,24 @@ watch(
}
);

watch(
() => props.autoOpen,
autoOpen => {
if (autoOpen !== false && !isActive.value) {
void openMetadata();
}
}
);

onMounted(() => {
window.addEventListener('beforeunload', confirmUnload);
unregisterSurfaceHandler = props.editorSurface.registerTargetHandler(target.value.key, {
isDirty: () => isDirty.value,
discard: resetDraft
});
void openMetadata();
if (props.autoOpen !== false) {
void openMetadata();
}
});

onUnmounted(() => {
Expand Down Expand Up @@ -369,6 +390,11 @@ const fieldErrors = (key: string): string[] => validationErrors.value[key] ?? []
<div>
<h2>Trip Settings</h2>
<p>{{ metadata.isPublic ? 'Public trip' : 'Private trip' }}</p>
<p>Share progress: {{ shareProgressSummary }}</p>
<div v-if="metadata.publicUrl || summaryProgressUrl" class="trip-editor-editor-summary__links">
<a v-if="metadata.publicUrl" :href="metadata.publicUrl" target="_blank" rel="noopener">Open public trip</a>
<a v-if="summaryProgressUrl" :href="summaryProgressUrl" target="_blank" rel="noopener">Open progress URL</a>
</div>
</div>
<button type="button" class="btn btn-outline-light btn-sm" @click="openMetadata">Edit Trip</button>
</section>
Expand Down
20 changes: 20 additions & 0 deletions ClientApps/trip-editor/src/components/RegionPlaceList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,24 @@ function toggleRegion(regionId: Guid): void {

collapsedRegionIds.value = next;
}

function moveAreaByKeyboard(regionId: Guid, areaId: Guid, offset: number): void {
if (props.searchActive || props.isOrdering) {
return;
}

const ids = [...(props.areaIdsByRegionId[regionId] ?? [])];
const index = ids.indexOf(areaId);
const nextIndex = index + offset;
if (index < 0 || nextIndex < 0 || nextIndex >= ids.length) {
return;
}

const previousIds = [...(props.state.areaOrderByRegionId[regionId] ?? [])];
const [id] = ids.splice(index, 1);
ids.splice(nextIndex, 0, id);
emit('areaReorder', regionId, ids, previousIds);
}
</script>

<template>
Expand Down Expand Up @@ -335,6 +353,8 @@ function toggleRegion(regionId: Guid): void {
title="Drag to reorder area"
aria-label="Drag to reorder area"
:disabled="props.searchActive || props.isOrdering"
@keydown.arrow-up.prevent="moveAreaByKeyboard(region.id, area.id, -1)"
@keydown.arrow-down.prevent="moveAreaByKeyboard(region.id, area.id, 1)"
>
<span aria-hidden="true">::</span>
</button>
Expand Down
35 changes: 33 additions & 2 deletions ClientApps/trip-editor/src/components/SegmentManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const lastSavedAt = ref<string | null>(null);
const routeMapWork = reactive<SegmentRouteMapWorkState>({ route: null, stopEdit: null });
let unregisterHandler: (() => void) | null = null;
let sortable: { destroy: () => void } | null = null;
let sortableRetry: number | null = null;
let reorderSnapshotIds: Guid[] | null = null;

const activeSegment = computed(() => (draft.id ? props.state.segmentsById[draft.id] ?? null : null));
Expand Down Expand Up @@ -251,9 +252,35 @@ async function reorder(ids: Guid[], previousIds: Guid[]): Promise<void> {
}
}

async function moveSegmentByKeyboard(segmentId: Guid, offset: number): Promise<void> {
if (props.searchActive || isOrdering.value) {
return;
}

const ids = props.segments.map(segment => segment.id);
const index = ids.indexOf(segmentId);
const nextIndex = index + offset;
if (index < 0 || nextIndex < 0 || nextIndex >= ids.length) {
return;
}

const previousIds = props.state.segmentOrder.filter(id => props.state.segmentsById[id]);
const [id] = ids.splice(index, 1);
ids.splice(nextIndex, 0, id);
await reorder(ids, previousIds);
}

function attachSortable(): void {
destroySortable();
if (props.searchActive || !segmentList.value || !window.Sortable) {
if (props.searchActive || !segmentList.value) {
return;
}

if (!window.Sortable) {
sortableRetry = window.setTimeout(() => {
sortableRetry = null;
attachSortable();
}, 50);
return;
}

Expand All @@ -278,6 +305,10 @@ function attachSortable(): void {
}

function destroySortable(): void {
if (sortableRetry !== null) {
window.clearTimeout(sortableRetry);
sortableRetry = null;
}
sortable?.destroy();
sortable = null;
}
Expand Down Expand Up @@ -352,7 +383,7 @@ function modeText(segment: EditorSegment): string {

<ul v-if="segments.length > 0" :key="segmentListKey" ref="segmentList" class="trip-editor-segments">
<li v-for="segment in segments" :key="segment.id" class="trip-editor-segment-row" :data-segment-id="segment.id">
<button type="button" class="trip-editor-icon-button trip-editor-segment-drag-handle" title="Drag to reorder segment" aria-label="Drag to reorder segment">↕</button>
<button type="button" class="trip-editor-icon-button trip-editor-segment-drag-handle" title="Drag to reorder segment" aria-label="Drag to reorder segment" @keydown.arrow-up.prevent="moveSegmentByKeyboard(segment.id, -1)" @keydown.arrow-down.prevent="moveSegmentByKeyboard(segment.id, 1)">↕</button>
<button type="button" class="trip-editor-icon-button" :title="hiddenSegmentIds.has(segment.id) ? 'Show segment' : 'Hide segment'" :aria-label="hiddenSegmentIds.has(segment.id) ? 'Show segment' : 'Hide segment'" @click="toggleVisibility(segment)">{{ hiddenSegmentIds.has(segment.id) ? '○' : '●' }}</button>
<button type="button" class="trip-editor-list-button" @click="openEdit(segment)">
<span>{{ segmentLabel(segment) }}</span>
Expand Down
Loading
Loading