Ask before the filter editors lose your changes

Both editors on the Filters & rules page kept their edits in component
state, so every way out of the page threw them away without a word: a
settings link, the app rail, even the Rules/Scripts switch. The only sign
there had been anything to lose was a Save button that a screenful of
rules had already pushed below the fold.

Editors now register what they have pending, and every in-app navigation
asks first -- offering to save, rather than making "leave without saving"
the easy answer and saving the one you have to back out and find. Wouter
routes links, redirects and navigate() through one place, so the guard
holds for the app rail and the settings nav without either knowing an
editor exists. The Rules/Scripts switch asks for itself, since it never
reaches the router. Reload and tab close get the browser's own prompt.

The save bar is pinned to the foot of the pane, so "Unsaved changes" is
on screen whether or not the rules fit in the window.

Two things fixed on the way past, both in the raw script editor: saving
cleared only the selection, which left the editor open with the name
unlocked so a second save created a duplicate script instead of updating
the one just written; and the pair of identical nested conditions that
decided whether the editor was open at all is now the one question it was
asking twice.

Fixes #175
This commit is contained in:
2026-09-01 07:05:17 -07:00
parent b63f53c55b
commit d3e173c9c1
5 changed files with 357 additions and 41 deletions
+14 -2
View File
@@ -730,7 +730,7 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
.settings-layout { display: grid; grid-template-columns: 240px 1fr; height: 100%; min-height: 0; flex: 1; }
.settings-nav { border-right: 1px solid var(--border); padding: 12px 8px; overflow-y: auto; }
.settings-nav .nav-item { margin-right: 0; border-radius: var(--radius-sm); }
.settings-content { overflow-y: auto; padding: 24px 32px 64px; max-width: 860px; }
.settings-content { --pad-b: 64px; overflow-y: auto; padding: 24px 32px var(--pad-b); max-width: 860px; }
.settings-content h1 { margin: 0 0 4px; font-size: 1.5em; font-weight: 650; }
.settings-content h2 { margin: 28px 0 12px; font-size: 1.05em; font-weight: 650; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
.settings-content .lead { color: var(--fg-muted); margin: 0 0 20px; }
@@ -754,6 +754,18 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
.rule-card.drop-below { box-shadow: inset 0 -3px 0 0 var(--accent); }
.rule-card .drag-handle { display: flex; align-items: center; padding: 2px; margin-left: -4px; color: var(--fg-faint); cursor: grab; touch-action: none; }
.rule-card .drag-handle:active { cursor: grabbing; }
/* The bar that says whether there is unsaved work, kept where it can be read.
Unpinned it sat after the rules, which on a full list means off the bottom of
the window: the one indicator that anything was pending was the one thing you
had to scroll to find (issue #175).
The offset is what pins it flush. A sticky `bottom: 0` stops at the scroll
container's padding box, which leaves the pane's bottom padding as a strip
below the bar for rules to scroll through; sticking to minus that padding
closes it, on the narrow layout's larger padding as well. */
.save-bar { position: sticky; bottom: calc(var(--pad-b, 0px) * -1); z-index: 2; margin-top: 12px; padding: 12px 0; background: var(--bg); border-top: 1px solid var(--border); }
.save-bar .unsaved { color: var(--warn); font-weight: 600; font-size: .9em; }
.rule-row { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 8px; align-items: center; margin-bottom: 8px; }
.rule-row.actions { grid-template-columns: 1fr 2fr auto; }
/* A header typed by hand needs a box of its own, alongside the comparator. */
@@ -1017,7 +1029,7 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
.settings-layout.section .settings-nav { display: none; }
.settings-layout.root .settings-nav { display: block; border-right: 0; }
.settings-layout.root .settings-content { display: none; }
.settings-content { padding: 16px 16px 80px; }
.settings-content { --pad-b: 80px; padding: 16px 16px var(--pad-b); }
.contacts-layout { grid-template-columns: 1fr; }
.contacts-books { display: none; }
.contacts-layout.detail .contacts-list { display: none; }