From 0f58966b317a3a8acada78517745c8f382aa0dc1 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 12 Aug 2026 22:16:58 +0200 Subject: [PATCH] milestone 23 s3: move srOnly into the shared style vocabulary --- README.md | 18 ++++++++++++++---- web/src/features/blocklists/BlocklistsPage.tsx | 13 +------------ web/src/features/clients/ClientsPage.tsx | 13 +------------ web/src/features/local/RecordsTab.tsx | 13 +------------ web/src/features/local/ZonesTab.tsx | 13 +------------ web/src/features/rules/RulesPage.tsx | 13 +------------ web/src/features/upstreams/UpstreamsPage.tsx | 13 +------------ web/src/ui/styles.ts | 13 +++++++++++++ 8 files changed, 33 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 87e6c56..8e04e41 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,19 @@ # nxdns -A self-hosted DNS sinkhole for a household LAN, written in Zig 0.16. One -static musl binary, SQLite for state, a Raspberry Pi 5 as the reference -target. It answers your network's DNS, blocks what you tell it to, and shows -you what asked for what. +nxdns is a DNS sinkhole for your LAN. It blocks the names you do not want, and +forwards the rest over an encrypted connection. + +```mermaid +flowchart LR + devices["Your devices"] -- "DNS query" --> nxdns["nxdns"] + nxdns -- "answer" --> devices + nxdns -- "allowed" --> upstream["Upstream resolvers
DoH / DoT"] + upstream -- "answer" --> nxdns + nxdns -- "blocked" --> sink["0.0.0.0 / NXDOMAIN"] +``` + +One static Zig binary. SQLite holds the state and the query log, which the web +UI, the REST API and `/metrics` read. ## Features diff --git a/web/src/features/blocklists/BlocklistsPage.tsx b/web/src/features/blocklists/BlocklistsPage.tsx index 13970a6..b194762 100644 --- a/web/src/features/blocklists/BlocklistsPage.tsx +++ b/web/src/features/blocklists/BlocklistsPage.tsx @@ -79,17 +79,6 @@ const styles = stylex.create({ dimWhenDisabled: { opacity: { default: 1, ":disabled": 0.5 }, }, - srOnly: { - position: "absolute", - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: "hidden", - clipPath: "inset(50%)", - whiteSpace: "nowrap", - borderWidth: 0, - }, }); export default function BlocklistsPage() { @@ -170,7 +159,7 @@ export default function BlocklistsPage() { Skipped regex Last updated - Actions + Actions diff --git a/web/src/features/clients/ClientsPage.tsx b/web/src/features/clients/ClientsPage.tsx index 9b760b7..1fa5dd3 100644 --- a/web/src/features/clients/ClientsPage.tsx +++ b/web/src/features/clients/ClientsPage.tsx @@ -96,17 +96,6 @@ const styles = stylex.create({ * The accessible name of the actions column, kept out of the visual table * without leaving the accessibility tree. */ - srOnly: { - position: "absolute", - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: "hidden", - clipPath: "inset(50%)", - whiteSpace: "nowrap", - borderWidth: 0, - }, }); export default function ClientsPage() { @@ -138,7 +127,7 @@ export default function ClientsPage() { First seen Last seen - Actions + Actions diff --git a/web/src/features/local/RecordsTab.tsx b/web/src/features/local/RecordsTab.tsx index 667095d..97cc588 100644 --- a/web/src/features/local/RecordsTab.tsx +++ b/web/src/features/local/RecordsTab.tsx @@ -91,17 +91,6 @@ const styles = stylex.create({ dimWhenDisabled: { opacity: { default: 1, ":disabled": 0.5 }, }, - srOnly: { - position: "absolute", - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: "hidden", - clipPath: "inset(50%)", - whiteSpace: "nowrap", - borderWidth: 0, - }, }); function RecordForm({ @@ -268,7 +257,7 @@ export default function RecordsTab() { TTL - Actions + Actions diff --git a/web/src/features/local/ZonesTab.tsx b/web/src/features/local/ZonesTab.tsx index 94505b6..eefb36c 100644 --- a/web/src/features/local/ZonesTab.tsx +++ b/web/src/features/local/ZonesTab.tsx @@ -87,17 +87,6 @@ const styles = stylex.create({ dimWhenDisabled: { opacity: { default: 1, ":disabled": 0.5 }, }, - srOnly: { - position: "absolute", - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: "hidden", - clipPath: "inset(50%)", - whiteSpace: "nowrap", - borderWidth: 0, - }, }); function ZoneForm({ @@ -232,7 +221,7 @@ export default function ZonesTab() { Resolver - Actions + Actions diff --git a/web/src/features/rules/RulesPage.tsx b/web/src/features/rules/RulesPage.tsx index e6ce456..6946610 100644 --- a/web/src/features/rules/RulesPage.tsx +++ b/web/src/features/rules/RulesPage.tsx @@ -78,17 +78,6 @@ const styles = stylex.create({ submitRow: { display: "flex", }, - srOnly: { - position: "absolute", - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: "hidden", - clipPath: "inset(50%)", - whiteSpace: "nowrap", - borderWidth: 0, - }, }); export default function RulesPage() { @@ -137,7 +126,7 @@ export default function RulesPage() { Group Created - Actions + Actions diff --git a/web/src/features/upstreams/UpstreamsPage.tsx b/web/src/features/upstreams/UpstreamsPage.tsx index ae4abba..4af34f1 100644 --- a/web/src/features/upstreams/UpstreamsPage.tsx +++ b/web/src/features/upstreams/UpstreamsPage.tsx @@ -53,17 +53,6 @@ const styles = stylex.create({ dimWhenDisabled: { opacity: { default: 1, ":disabled": 0.5 }, }, - srOnly: { - position: "absolute", - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: "hidden", - clipPath: "inset(50%)", - whiteSpace: "nowrap", - borderWidth: 0, - }, }); export default function UpstreamsPage() { @@ -127,7 +116,7 @@ export default function UpstreamsPage() { Enabled TLS name - Actions + Actions diff --git a/web/src/ui/styles.ts b/web/src/ui/styles.ts index 6046d31..b48971a 100644 --- a/web/src/ui/styles.ts +++ b/web/src/ui/styles.ts @@ -151,6 +151,19 @@ export const styles = stylex.create({ color: colors.dangerText, }, + /** Visible to a screen reader only; the element keeps its place in the a11y tree. */ + srOnly: { + position: "absolute", + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: "hidden", + clipPath: "inset(50%)", + whiteSpace: "nowrap", + borderWidth: 0, + }, + th: { borderBottomWidth: 1, borderBottomStyle: "solid",