diff --git a/app/cypress/e2e/keyboard-shortcuts.cy.js b/app/cypress/e2e/keyboard-shortcuts.cy.js
index df7f8df6..fd2835f9 100644
--- a/app/cypress/e2e/keyboard-shortcuts.cy.js
+++ b/app/cypress/e2e/keyboard-shortcuts.cy.js
@@ -17,18 +17,42 @@ describe('Keyboard Shortcuts', () => {
body: {settings: {}}
}).as('getSettings');
- // Mock tags API
- cy.intercept('GET', '/api/tags', {
+ /*
+ * `/api/tag` and `/api/events/feed`, which are the URLs the app actually requests.
+ *
+ * These read `/api/tags` and `/api/events` before -- neither of which anything calls,
+ * so both mocks matched nothing and the real calls fell through. In CI, where the
+ * React app is served with no API behind it, they failed and api.js raised an
+ * "Unexpected server response" toast, which then covered the search modal's input and
+ * failed an occlusion-sensitive assertion. Locally the API answered and the spec
+ * passed, which is why it only ever broke on CI.
+ */
+ cy.intercept('GET', '/api/tag*', {
statusCode: 200,
body: {tags: []}
}).as('getTags');
- // Mock events API
- cy.intercept('GET', '/api/events', {
+ cy.intercept('GET', '/api/events/feed*', {
statusCode: 200,
- body: {events: []}
+ // `now` as well as `events`: Events.js reads both off the response.
+ body: {events: [], now: new Date(0).toISOString()}
}).as('getEvents');
+ /*
+ * The other two the home page calls. `file_groups` is reached as
+ * `data['totals']['file_groups']`, so an empty object here would throw rather than
+ * toast -- the shape has to be right, which is why a blanket `{}` stub does not work.
+ */
+ cy.intercept('POST', '/api/files/search', {
+ statusCode: 200,
+ body: {file_groups: [], totals: {file_groups: 0}}
+ }).as('searchFiles');
+
+ cy.intercept('GET', '/api/files/worker_status', {
+ statusCode: 200,
+ body: {status: {}}
+ }).as('getWorkerStatus');
+
// Mock search suggestions API
cy.intercept('POST', '/api/search/suggestions', {
statusCode: 200,
@@ -56,25 +80,13 @@ describe('Keyboard Shortcuts', () => {
cy.get('[role="dialog"]').should('be.visible');
/*
- * Take down any toast before the assertion below, which is occlusion-sensitive.
- *
- * Toasts paint above modals on purpose -- z-index 400 against the modal stack's
- * 200 -- so that an error raised while a modal is open stays visible. The search
- * modal is tall, so it sits near the top of the window, and a top-right toast
- * covers the right-hand 192px of its 365px input. Cypress occlusion-checks a
- * `position: fixed` element, so `should('be.visible')` on that input fails.
- *
- * That is not this spec's subject. It passed locally and failed in CI, where the
- * React app is served with no API behind it: `/api/tag`, `/api/events/feed`,
- * `/api/files/search` and `/api/files/worker_status` are not mocked here, they
- * fail, and api.js raises "Unexpected server response". The overlap itself is an
- * accepted trade-off -- see the toast/modal note in ui.css -- so the environment is
- * made quiet rather than the assertion weakened.
- *
- * Removed rather than dismissed by clicking: a click races the next failing call.
+ * Occlusion-sensitive: Cypress checks whether a `position: fixed` element is
+ * covered, and a toast at top-right covers the right-hand 192px of this 365px
+ * input. Toasts paint above modals on purpose -- z-index 400 against the modal
+ * stack's 200 -- so an error raised while a modal is open stays visible, and that
+ * overlap is an accepted trade-off rather than something this spec should assert
+ * about. The endpoints are all mocked in `beforeEach` so no toast is raised.
*/
- cy.get('body').then(($body) => $body.find('.mantine-Notifications-root').remove());
-
cy.get('[role="dialog"] input').should('be.visible');
});
diff --git a/app/src/App.css b/app/src/App.css
index bf7afe06..20349c40 100644
--- a/app/src/App.css
+++ b/app/src/App.css
@@ -216,66 +216,18 @@ div.inverted {
background-color: #1B1C1D;
}
-.toggle {
- position: relative;
- display: inline-block;
- width: 3.0625rem;
- height: 1.4375rem;
-}
-
-.toggle input {
- opacity: 0;
- width: 0;
- height: 0;
-}
-
-.slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgb(166, 166, 166);
- -webkit-transition: .4s;
- transition: .4s;
- border-radius: 2.125rem;
-}
-
-.slider:before {
- position: absolute;
- content: "";
- height: 1.375rem;
- width: 1.375rem;
- left: 0;
- bottom: 0;
- background-color: white;
- -webkit-transition: .4s;
- transition: .4s;
- border-radius: 50%;
-}
-
-.slider.disabled:before {
- background-color: #CCCCCC;
-}
-
-input:checked + .slider {
- background-color: #6435C9FF;
-}
-
-input.inverted:checked + .slider.inverted {
- background-color: #6435C9AA;
-}
-
-input.disabled + .slider {
- background-color: rgb(147, 147, 147);
-}
-
-input:checked + .slider:before {
- -webkit-transform: translateX(26px);
- -ms-transform: translateX(26px);
- transform: translateX(26px);
-}
+/*
+ * The hand-rolled `.toggle` / `.slider` switch is gone.
+ *
+ * Nothing referenced either class: `Toggle` is a Mantine Switch, and the only remaining
+ * matches for the words were the `classList.toggle` DOM method and the controller's own
+ * templates, which do not load this stylesheet.
+ *
+ * It is deleted rather than finished because it was already half-converted and broken: the
+ * track and knob became rem while the checked-state travel stayed `translateX(26px)`, so on a
+ * 54px track the thumb would have stopped ~4px short. Dead CSS that is also wrong is a trap
+ * for whoever revives it, and it also hardcoded #6435C9 and white, which no theme can reach.
+ */
.inverted-progress-text.ui.indicating.progress .bar > .progress,
.inverted-progress-text.ui.progress > .label {
@@ -306,7 +258,8 @@ input:checked + .slider:before {
}
.preview-toolbar-tags {
- flex: 1 1 200px;
+ /* rem so the basis grows with the tags it holds. */
+ flex: 1 1 12.5rem;
min-width: 0;
}
diff --git a/app/src/components/MapViewer.js b/app/src/components/MapViewer.js
index cea9be3d..f00b519d 100644
--- a/app/src/components/MapViewer.js
+++ b/app/src/components/MapViewer.js
@@ -264,15 +264,15 @@ function LayerControl({map, scaleUnit, onScaleUnitChange, visibilityRef}) {
// every theme, same as the nav bar.
const panelStyle = {
position: "absolute",
- top: 10,
- left: 10,
+ top: "0.625rem",
+ left: "0.625rem",
zIndex: 1000,
background: "var(--panel)",
border: "1px solid var(--border)",
color: "var(--text)",
- maxHeight: "calc(100vh - 120px)",
+ maxHeight: "calc(100vh - 7.5rem)",
overflowY: "auto",
- minWidth: iconOnly ? 0 : 200,
+ minWidth: iconOnly ? 0 : "12.5rem",
fontSize: '0.8125rem',
};
@@ -280,10 +280,10 @@ function LayerControl({map, scaleUnit, onScaleUnitChange, visibilityRef}) {
? {
padding: 0, cursor: "pointer", userSelect: "none",
display: "flex", alignItems: "center", justifyContent: "center",
- width: 40, height: 40,
+ width: "2.5rem", height: "2.5rem",
}
: {
- padding: "8px 12px", cursor: "pointer", fontWeight: 600,
+ padding: "0.5rem 0.75rem", cursor: "pointer", fontWeight: 600,
display: "flex", justifyContent: "space-between", alignItems: "center",
borderBottom: collapsed ? "none" : "1px solid var(--border)", userSelect: "none",
};
@@ -302,9 +302,9 @@ function LayerControl({map, scaleUnit, onScaleUnitChange, visibilityRef}) {
>}
- {!collapsed &&