admin: title-only information becomes visible text

the client address follows its name as visible muted text in the query tables, the config lock indicator prints its reason beside the tag except in table rows where a page-level note explains the lock instead, and the locked delete buttons describe themselves through that one visible note. the chart legend tooltip is deleted because a named client is deliberately not addressed in the chart, and the dead series address field went with it. titles that merely repeat visible copyable text stay.
This commit is contained in:
2026-08-29 13:03:30 +02:00
parent 207252acee
commit c65d92d8f8
9 changed files with 154 additions and 51 deletions
@@ -1,8 +1,20 @@
import * as stylex from "@stylexjs/stylex";
import { colors } from "@/ui/tokens.stylex";
import { useAuthority } from "./authority";
import { useAuthority, type Authority } from "./authority";
const styles = stylex.create({
row: {
display: "inline-flex",
alignItems: "baseline",
flexWrap: "wrap",
gap: "0.375rem",
},
/** The sentence is secondary to the word, and wraps rather than stretching a row. */
reason: {
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textMuted,
},
tag: {
marginLeft: "0.5rem",
borderWidth: 1,
@@ -26,20 +38,34 @@ const styles = stylex.create({
* The word is real text, not colour or an icon, so a screen reader announces
* the reason the control will not answer.
*/
export default function ConfigLockIndicator() {
/**
* Why a configuration control will not answer. Exported because a page that
* shows the compact tag has to print this sentence itself, once, somewhere the
* tag can point at.
*/
export function lockReason(authority: Authority): string {
if (authority.state === "pending") return "Checking which configuration source this server obeys";
if (authority.state === "failed") return "Configuration status unavailable, so edits are held back";
return `Managed by ${authority.status.path ?? "the configuration file"}; edit the file and restart nxdns`;
}
export default function ConfigLockIndicator({ compact = false }: { compact?: boolean }) {
const authority = useAuthority();
if (authority.state === "resolved" && authority.status.authority === "database") return null;
const reason =
authority.state === "pending"
? "Checking which configuration source this server obeys"
: authority.state === "failed"
? "Configuration status unavailable, so edits are held back"
: `Managed by ${authority.status.path ?? "the configuration file"}; edit the file and restart nxdns`;
const reason = lockReason(authority);
// A compact caller has no room for the sentence and must print it once
// nearby instead: the Clients table would otherwise repeat it down every row.
if (compact) return <span {...stylex.props(styles.tag)}>Locked</span>;
// Everywhere else the reason is visible text rather than a `title` or an
// `aria-label`. A tooltip reaches neither a keyboard nor a touch screen, and
// screen-reader-only text is the same failure pointed the other way.
return (
<span title={reason} aria-label={`Locked. ${reason}.`} {...stylex.props(styles.tag)}>
Locked
<span {...stylex.props(styles.row)}>
<span {...stylex.props(styles.tag)}>Locked</span>
<span {...stylex.props(styles.reason)}>{reason}</span>
</span>
);
}