Drawing rail: fix phone layout, stray pointer, and make it movable

Three problems reported from a device after v15, two of them the same mistake.

Phone layout filled the screen. The strip's rule and ol-ext's
`.ol-control.ol-bar.ol-left` both weigh (0,3,0), so stylesheet order decided
which won — and the built app loads them in the opposite order from the test
harness, which is why this passed review and failed on the phone. With ol-ext
winning, `top: 50%` survived next to `bottom: 12px` and the bar stretched
between them. The rail's selectors now carry enough classes to win outright
rather than by luck; both orders are verified.

Stray dark shape beside the option bar was ol-ext's pointer triangle. Same
cause: `.ol-editbar-rail .ol-option-bar::before` (0,2,1) never beat
`.ol-control.ol-bar.ol-left .ol-option-bar:before` (0,4,1).

The rail covered the settings toggle. `.offcanvas-toggle-left` sits at
left:10px, vertically centred — precisely where ol-ext docks a left bar, so the
rail landed on top of it. The rail now starts at left:66px, clear of the 44px
toggle, and can be dragged anywhere by a grip at its top. The position is
remembered in localStorage, clamped so the rail cannot be pushed off-map, and
reset by double-clicking the grip. Dragging is limited to the grip so a
mis-aimed tap moves nothing, and is off below the phone breakpoint where the
bar is docked above the dock.

Service Worker v15 -> v16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ekke 2026-08-20 15:42:29 +02:00
parent e9bb4d81c9
commit 19dc000efe
10 changed files with 272 additions and 33 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/index-4Lh4FMTD.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

65
dist/index.html vendored
View File

@ -1536,7 +1536,11 @@
instead of dropping downwards. We keep that behaviour and override only instead of dropping downwards. We keep that behaviour and override only
the layout. the layout.
================================================================ */ ================================================================ */
.ol-editbar.ol-editbar-rail { .ol-control.ol-bar.ol-editbar.ol-editbar-rail {
/* Clear of the left off-canvas toggle, which sits at left:10px and is
44px wide, vertically centred — exactly where ol-ext docks a left bar.
Officers can drag the rail anywhere from here; see the grip below. */
left: 66px;
display: grid; display: grid;
grid-template-columns: repeat(2, 30px); grid-template-columns: repeat(2, 30px);
gap: 3px; gap: 3px;
@ -1564,6 +1568,51 @@
margin: 0; margin: 0;
} }
/* --- drag grip ---------------------------------------------------
The rail can be moved: officers work at different zoom levels and on
different hands, and a fixed panel always covers something. Dragging is
deliberately restricted to this grip so a mis-aimed tap on a tool never
moves the palette instead of selecting.
---------------------------------------------------------------- */
.ol-editbar-rail .ol-rail-grip {
grid-column: 1 / -1;
display: grid;
place-items: center;
height: 18px;
margin: -2px 0 2px;
border-radius: 6px;
color: var(--muted-foreground);
cursor: grab;
touch-action: none;
-webkit-tap-highlight-color: transparent;
}
.ol-editbar-rail .ol-rail-grip:hover {
background: var(--muted);
color: var(--foreground);
}
.ol-editbar-rail .ol-rail-grip:active,
.ol-editbar-rail.is-dragging .ol-rail-grip {
cursor: grabbing;
}
.ol-editbar-rail .ol-rail-grip::before {
content: "";
width: 22px;
height: 4px;
border-radius: 2px;
background: currentColor;
opacity: 0.45;
}
.ol-editbar-rail.is-dragging {
opacity: 0.9;
user-select: none;
}
/* No dragging on phones — the strip is docked above the dock bar. */
@media (max-width: 576px) {
.ol-editbar-rail .ol-rail-grip {
display: none;
}
}
/* Group heading — spans both columns. */ /* Group heading — spans both columns. */
.ol-editbar-rail .ol-rail-label { .ol-editbar-rail .ol-rail-label {
grid-column: 1 / -1; grid-column: 1 / -1;
@ -1680,8 +1729,14 @@
margin: 0 0 0 8px !important; margin: 0 0 0 8px !important;
white-space: nowrap; white-space: nowrap;
} }
.ol-editbar-rail .ol-option-bar::before { /* ol-ext draws a little pointer triangle on the option bar. It reads as a
stray dark button next to the flyout, and it is declared at
`.ol-control.ol-bar.ol-left .ol-option-bar:before` (0,4,1) — so the
selector that hides it has to be at least as specific. */
.ol-control.ol-bar.ol-editbar-rail.ol-left .ol-option-bar::before,
.ol-control.ol-bar.ol-editbar-rail.ol-left .ol-option-bar::after {
display: none; display: none;
content: none;
} }
.ol-editbar-rail .ol-option-bar .ol-control { .ol-editbar-rail .ol-option-bar .ol-control {
display: inline-block !important; display: inline-block !important;
@ -1725,7 +1780,7 @@
/* `.ol-left` is repeated here for specificity: ol-ext's own /* `.ol-left` is repeated here for specificity: ol-ext's own
`.ol-control.ol-bar.ol-left` sets top/transform, and without matching `.ol-control.ol-bar.ol-left` sets top/transform, and without matching
it the rail stays vertically centred and stretches to `bottom`. */ it the rail stays vertically centred and stretches to `bottom`. */
.ol-editbar.ol-editbar-rail.ol-left { .ol-control.ol-bar.ol-editbar.ol-editbar-rail.ol-left {
/* NOTE: display must NOT be !important — ol-ext toggles the bar with an /* NOTE: display must NOT be !important — ol-ext toggles the bar with an
inline display:none/'' (setVisible). An !important here would beat inline display:none/'' (setVisible). An !important here would beat
that and keep the toolbar visible even when Draw mode is off. */ that and keep the toolbar visible even when Draw mode is off. */
@ -1748,7 +1803,7 @@
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scrollbar-width: none; scrollbar-width: none;
} }
.ol-editbar.ol-editbar-rail.ol-left::-webkit-scrollbar { .ol-control.ol-bar.ol-editbar.ol-editbar-rail.ol-left::-webkit-scrollbar {
display: none; display: none;
} }
@ -1820,7 +1875,7 @@
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-9k1yuCxX.js"></script> <script type="module" crossorigin src="/assets/index-4Lh4FMTD.js"></script>
<link rel="modulepreload" crossorigin href="/assets/openlayers-J9qS6Th1.js"> <link rel="modulepreload" crossorigin href="/assets/openlayers-J9qS6Th1.js">
<link rel="modulepreload" crossorigin href="/assets/pako-Xa-UToif.js"> <link rel="modulepreload" crossorigin href="/assets/pako-Xa-UToif.js">
<link rel="modulepreload" crossorigin href="/assets/geotiff-BaoeLn6q.js"> <link rel="modulepreload" crossorigin href="/assets/geotiff-BaoeLn6q.js">

11
dist/sw.js vendored
View File

@ -80,7 +80,16 @@
// Icon. On phones it becomes a horizontally scrolling strip above the // Icon. On phones it becomes a horizontally scrolling strip above the
// dock, which retires the previous two-row wrapping workaround. // dock, which retires the previous two-row wrapping workaround.
// New hashed bundle + updated index.html shell. // New hashed bundle + updated index.html shell.
const CACHE_VERSION = 'v15'; // v16: Three fixes to the v15 rail, all reported from the field. The phone
// layout stretched down the whole screen because its rule only tied with
// ol-ext's on specificity, so stylesheet order decided the winner and the
// built app ordered them the other way round from the test harness. A
// stray dark shape beside the option bar was ol-ext's pointer triangle,
// hidden by a selector that was likewise not specific enough. And the rail
// covered the left off-canvas toggle, which sits at exactly the position
// ol-ext docks a left bar to — so the rail now starts clear of it and can
// be dragged anywhere by its grip, with the position remembered.
const CACHE_VERSION = 'v16';
const SHELL_CACHE = `shell-${CACHE_VERSION}`; const SHELL_CACHE = `shell-${CACHE_VERSION}`;
const MODULES_CACHE = `modules-${CACHE_VERSION}`; const MODULES_CACHE = `modules-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`; const API_CACHE = `api-${CACHE_VERSION}`;

View File

@ -1536,7 +1536,11 @@
instead of dropping downwards. We keep that behaviour and override only instead of dropping downwards. We keep that behaviour and override only
the layout. the layout.
================================================================ */ ================================================================ */
.ol-editbar.ol-editbar-rail { .ol-control.ol-bar.ol-editbar.ol-editbar-rail {
/* Clear of the left off-canvas toggle, which sits at left:10px and is
44px wide, vertically centred — exactly where ol-ext docks a left bar.
Officers can drag the rail anywhere from here; see the grip below. */
left: 66px;
display: grid; display: grid;
grid-template-columns: repeat(2, 30px); grid-template-columns: repeat(2, 30px);
gap: 3px; gap: 3px;
@ -1564,6 +1568,51 @@
margin: 0; margin: 0;
} }
/* --- drag grip ---------------------------------------------------
The rail can be moved: officers work at different zoom levels and on
different hands, and a fixed panel always covers something. Dragging is
deliberately restricted to this grip so a mis-aimed tap on a tool never
moves the palette instead of selecting.
---------------------------------------------------------------- */
.ol-editbar-rail .ol-rail-grip {
grid-column: 1 / -1;
display: grid;
place-items: center;
height: 18px;
margin: -2px 0 2px;
border-radius: 6px;
color: var(--muted-foreground);
cursor: grab;
touch-action: none;
-webkit-tap-highlight-color: transparent;
}
.ol-editbar-rail .ol-rail-grip:hover {
background: var(--muted);
color: var(--foreground);
}
.ol-editbar-rail .ol-rail-grip:active,
.ol-editbar-rail.is-dragging .ol-rail-grip {
cursor: grabbing;
}
.ol-editbar-rail .ol-rail-grip::before {
content: "";
width: 22px;
height: 4px;
border-radius: 2px;
background: currentColor;
opacity: 0.45;
}
.ol-editbar-rail.is-dragging {
opacity: 0.9;
user-select: none;
}
/* No dragging on phones — the strip is docked above the dock bar. */
@media (max-width: 576px) {
.ol-editbar-rail .ol-rail-grip {
display: none;
}
}
/* Group heading — spans both columns. */ /* Group heading — spans both columns. */
.ol-editbar-rail .ol-rail-label { .ol-editbar-rail .ol-rail-label {
grid-column: 1 / -1; grid-column: 1 / -1;
@ -1680,8 +1729,14 @@
margin: 0 0 0 8px !important; margin: 0 0 0 8px !important;
white-space: nowrap; white-space: nowrap;
} }
.ol-editbar-rail .ol-option-bar::before { /* ol-ext draws a little pointer triangle on the option bar. It reads as a
stray dark button next to the flyout, and it is declared at
`.ol-control.ol-bar.ol-left .ol-option-bar:before` (0,4,1) — so the
selector that hides it has to be at least as specific. */
.ol-control.ol-bar.ol-editbar-rail.ol-left .ol-option-bar::before,
.ol-control.ol-bar.ol-editbar-rail.ol-left .ol-option-bar::after {
display: none; display: none;
content: none;
} }
.ol-editbar-rail .ol-option-bar .ol-control { .ol-editbar-rail .ol-option-bar .ol-control {
display: inline-block !important; display: inline-block !important;
@ -1725,7 +1780,7 @@
/* `.ol-left` is repeated here for specificity: ol-ext's own /* `.ol-left` is repeated here for specificity: ol-ext's own
`.ol-control.ol-bar.ol-left` sets top/transform, and without matching `.ol-control.ol-bar.ol-left` sets top/transform, and without matching
it the rail stays vertically centred and stretches to `bottom`. */ it the rail stays vertically centred and stretches to `bottom`. */
.ol-editbar.ol-editbar-rail.ol-left { .ol-control.ol-bar.ol-editbar.ol-editbar-rail.ol-left {
/* NOTE: display must NOT be !important — ol-ext toggles the bar with an /* NOTE: display must NOT be !important — ol-ext toggles the bar with an
inline display:none/'' (setVisible). An !important here would beat inline display:none/'' (setVisible). An !important here would beat
that and keep the toolbar visible even when Draw mode is off. */ that and keep the toolbar visible even when Draw mode is off. */
@ -1748,7 +1803,7 @@
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scrollbar-width: none; scrollbar-width: none;
} }
.ol-editbar.ol-editbar-rail.ol-left::-webkit-scrollbar { .ol-control.ol-bar.ol-editbar.ol-editbar-rail.ol-left::-webkit-scrollbar {
display: none; display: none;
} }

View File

@ -80,7 +80,16 @@
// Icon. On phones it becomes a horizontally scrolling strip above the // Icon. On phones it becomes a horizontally scrolling strip above the
// dock, which retires the previous two-row wrapping workaround. // dock, which retires the previous two-row wrapping workaround.
// New hashed bundle + updated index.html shell. // New hashed bundle + updated index.html shell.
const CACHE_VERSION = 'v15'; // v16: Three fixes to the v15 rail, all reported from the field. The phone
// layout stretched down the whole screen because its rule only tied with
// ol-ext's on specificity, so stylesheet order decided the winner and the
// built app ordered them the other way round from the test harness. A
// stray dark shape beside the option bar was ol-ext's pointer triangle,
// hidden by a selector that was likewise not specific enough. And the rail
// covered the left off-canvas toggle, which sits at exactly the position
// ol-ext docks a left bar to — so the rail now starts clear of it and can
// be dragged anywhere by its grip, with the position remembered.
const CACHE_VERSION = 'v16';
const SHELL_CACHE = `shell-${CACHE_VERSION}`; const SHELL_CACHE = `shell-${CACHE_VERSION}`;
const MODULES_CACHE = `modules-${CACHE_VERSION}`; const MODULES_CACHE = `modules-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`; const API_CACHE = `api-${CACHE_VERSION}`;

View File

@ -1110,6 +1110,117 @@ export class MapView {
for (const member of members) ordered.appendChild(member); for (const member of members) ordered.appendChild(member);
} }
bar.appendChild(ordered); bar.appendChild(ordered);
// --- 3. drag grip -------------------------------------------------------
const grip = document.createElement('div');
grip.className = 'ol-rail-grip';
grip.title = 'Drag to move the toolbar — double-click to reset';
grip.setAttribute('role', 'button');
grip.setAttribute('aria-label', 'Move the drawing toolbar');
bar.insertBefore(grip, bar.firstChild);
this._makeToolbarDraggable(bar, grip);
}
/**
* Let the user drag the rail by its grip, and remember where they put it.
*
* A fixed palette always covers something at the default position it sits
* over the left off-canvas toggle on short screens, and officers work at
* different zoom levels and with different hands. Rather than pick a spot that
* is wrong slightly less often, the rail moves and stays where it is put.
*
* Only above the phone breakpoint: below it the bar is a strip docked above
* the bottom dock, where there is nowhere useful to drag it to.
*/
_makeToolbarDraggable(bar, grip) {
const STORAGE_KEY = 'editbar-position';
const phone = window.matchMedia('(max-width: 576px)');
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
/** Apply a position, clamped so the rail can never be dragged off-map. */
const place = (left, top) => {
const parent = bar.offsetParent || this.map.getTargetElement();
if (!parent) return;
const maxLeft = Math.max(0, parent.clientWidth - bar.offsetWidth - 4);
const maxTop = Math.max(0, parent.clientHeight - bar.offsetHeight - 4);
bar.style.left = `${clamp(left, 4, maxLeft)}px`;
bar.style.top = `${clamp(top, 4, maxTop)}px`;
// ol-ext centres a left-docked bar with translateY(-50%); once the user
// has positioned it, top is an absolute value and the offset must go.
bar.style.transform = 'none';
bar.style.bottom = 'auto';
};
const clearInline = () => {
bar.style.left = '';
bar.style.top = '';
bar.style.transform = '';
bar.style.bottom = '';
};
/** Restore a saved position, unless we are in the phone strip layout. */
const restore = () => {
if (phone.matches) { clearInline(); return; }
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
if (saved && Number.isFinite(saved.left) && Number.isFinite(saved.top)) {
place(saved.left, saved.top);
}
} catch {
// A corrupt entry is not worth failing the toolbar over.
}
};
let startX = 0;
let startY = 0;
let originLeft = 0;
let originTop = 0;
const onMove = (evt) => {
place(originLeft + (evt.clientX - startX), originTop + (evt.clientY - startY));
};
const onUp = (evt) => {
grip.releasePointerCapture?.(evt.pointerId);
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
bar.classList.remove('is-dragging');
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
left: parseFloat(bar.style.left) || 0,
top: parseFloat(bar.style.top) || 0,
}));
} catch {
// Private browsing or a full quota — the rail still moved this session.
}
};
grip.addEventListener('pointerdown', (evt) => {
if (phone.matches || evt.button !== 0) return;
evt.preventDefault();
const rect = bar.getBoundingClientRect();
const parentRect = (bar.offsetParent || this.map.getTargetElement()).getBoundingClientRect();
startX = evt.clientX;
startY = evt.clientY;
originLeft = rect.left - parentRect.left;
originTop = rect.top - parentRect.top;
bar.classList.add('is-dragging');
grip.setPointerCapture?.(evt.pointerId);
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
});
// Double-click the grip to put the rail back where it started.
grip.addEventListener('dblclick', () => {
try { localStorage.removeItem(STORAGE_KEY); } catch { /* ignore */ }
clearInline();
});
// Crossing the phone breakpoint swaps the layout, so drop or re-apply the
// inline position to match.
phone.addEventListener('change', restore);
restore();
} }
// ============================================================================ // ============================================================================