Match fields: "any of"/"any but" only with entries, never negate empty

The choice shows once the list has a row (empty is "any"), an empty
list is never sent as negated, a negated list is marked by a red bar
instead of red inputs (which read as errors), and the field's own label
names the choice for screen readers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-23 00:08:21 +01:00
1 parent 4676d17aea
commit 5ec943ebff
3 files changed
+20 -7

No files matched your search

+4 -1
View File
@@ -117,7 +117,10 @@ All notable changes to Housecarl are documented here. The format follows
this version.
- Negating a match is said first: the zone, From and To fields (rules
and NAT) start with "any of" / "any but" instead of a checkbox under
the list, and an "any but" list is tinted red.
the list, shown once the list has an entry, and an "any but" list is
marked with a red bar. An empty list is never saved as negated (it
matched everything anyway, but would have negated what was added to
it later).
- The rule dialog shows only the zone fields its chain can use: no
"Going out on" for input and early, no "Coming in on" for output
(they could only be refused). A zone left in a field the chain hides
+1 -1
View File
@@ -382,7 +382,7 @@ pre.wrap { white-space: pre-wrap; word-break: break-all; font-size: 12px; }
.negatable { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap; }
.negatable > .list-editor { flex: 1; min-width: 0; }
.negatable.negated > select { color: var(--err); border-color: var(--err); font-weight: 600; }
.negatable.negated .list-row input { background: var(--err-soft); color: var(--del-text); border-color: var(--err); }
.negatable.negated .list-rows { border-left: 3px solid var(--err); padding-left: 8px; }
input.short { width: 7em; }
.builtins { margin-bottom: 10px; line-height: 1.6; }
.policy { margin-top: 10px; }
+15 -5
View File
@@ -67,13 +67,23 @@ export function listEditor(values, opts = {}) {
// negatable puts "any of" / "any but" in front of a list editor, so a
// field says first whether it matches its entries or everything else;
// "any but" tints the list. value() is the list, negated() the choice.
// "any but" marks the list. The choice shows once the list has a row
// (empty is "any"), and an empty list is never negated. value() is the
// list, negated() the choice.
function negatable(list, not) {
const mode = select([["", "any of"], ["not", "any but"]], not ? "not" : "", { class: "inline", "aria-label": "match these, or everything but these" });
const box = h("div", { class: "negatable" + (not ? " negated" : "") }, mode, list);
mode.addEventListener("change", () => box.classList.toggle("negated", mode.value === "not"));
const mode = select([["", "any of"], ["not", "any but"]], not ? "not" : "", { class: "inline" });
const box = h("div", { class: "negatable" }, mode, list);
const rows = () => list.querySelector(".list-rows").children.length;
const update = () => {
mode.classList.toggle("hidden", rows() === 0);
box.classList.toggle("negated", mode.value === "not" && rows() > 0);
};
mode.addEventListener("change", update);
// Add and remove act on their own click first; this runs after.
list.addEventListener("click", update);
update();
box.value = list.value;
box.negated = () => mode.value === "not";
box.negated = () => mode.value === "not" && list.value().length > 0;
return box;
}