dorfteich/apps/web/src/components/vs-nfd.tsx
Claude Fable 5 9b1d4de426
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m17s
CI / Build container images (pull_request) Successful in 3m58s
CI / Auth e2e pack (pull_request) Failing after 8m26s
CI / Import/export fidelity gate (pull_request) Has been skipped
#245: mode hidden — hide profile-violating options, mark the hiding
In hidden (and later enforced) mode, catalog-listed controls whose only
purpose is enabling a violation are not rendered while their saved value
is compliant (the four master switches, the Nextcloud backup block);
value-listed selects keep only their compliant choices (registration
mode, new-page classification, upload policy, SVG policy). Every
affected section shows one accessible policy note (i18n de+en) so
policy is distinguishable from missing features. A value that was
already violating is surfaced exactly like in marked — never silently
hidden. The API stays unchanged; enforcement is #246. e2e: hidden half
of the marking pack (rows disappear, note visible, already-violating
row stays marked, axe WCAG A/AA clean) — verified live locally; CI runs
it against a second api (VS_NFD_MODE=hidden, same database) behind its
own static server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 19:10:51 +02:00

88 lines
3.2 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import {
VS_NFD_PROFILE,
describeCompliance,
isVsNfdCompliant,
type VsNfdMode,
type VsNfdProfileView,
} from '@dorfteich/shared';
import { useTranslation } from 'react-i18next';
import { apiGet } from '../lib/api';
/**
* VS-NfD profile marking (issue #244, ADR 0027): in mode `marked`, every
* catalog-listed control shows an accessible deviation marking. The check
* runs against the CURRENT control value (saved or not), so the operator
* is warned at the point of choice. Admin-only surfaces — the endpoint is
* Site-Admin-guarded, matching every caller.
*/
export function useVsNfdMarking(): {
mode: VsNfdMode;
view: VsNfdProfileView | undefined;
/** Translated marking text for a violating value, else undefined. */
markingFor: (key: string, value: unknown) => string | undefined;
/** True when the control must not be rendered (#245): mode hidden/
* enforced and the SAVED value is compliant. */
hides: (key: string, savedValue: unknown) => boolean;
} {
const { t } = useTranslation('settings');
const query = useQuery({
queryKey: ['admin', 'vs-nfd-profile'],
queryFn: () => apiGet<VsNfdProfileView>('/admin/system/vs-nfd-profile'),
staleTime: 60_000,
});
const mode = query.data?.mode ?? 'off';
return {
mode,
view: query.data,
markingFor(key, value) {
// Marking applies in `marked`, and in `hidden`/`enforced` to values
// that are ALREADY violating — an existing violation is never
// silently hidden (#245).
if (mode === 'off') return undefined;
const entry = VS_NFD_PROFILE.find((e) => e.scope === 'instance' && e.key === key);
if (!entry || isVsNfdCompliant(entry, value)) return undefined;
return t('admin.vsNfd.marking', { value: describeCompliance(entry.compliance) });
},
hides(key, savedValue) {
// The `hidden` treatment (#245, `enforced` renders the same, #246):
// a compliant control whose only purpose would be enabling a
// violation disappears; a violating one stays visible WITH its
// marking so the deviation is surfaced, never buried.
if (mode !== 'hidden' && mode !== 'enforced') return false;
const entry = VS_NFD_PROFILE.find((e) => e.scope === 'instance' && e.key === key);
return entry !== undefined && isVsNfdCompliant(entry, savedValue);
},
};
}
/**
* The one accessible per-section note that options are hidden by
* deployment policy (#245) — policy must be distinguishable from missing
* features for anyone reading the settings page.
*/
export function VsNfdHiddenNote(): React.JSX.Element {
const { t } = useTranslation('settings');
return (
<p className="field__hint vs-nfd-hidden-note">
<span aria-hidden="true">🛈 </span>
{t('admin.vsNfd.hiddenNote')}
</p>
);
}
/**
* The marking element for controls not wrapped in <Field> (checkbox rows):
* icon + text, colour never alone; wire `id` into the control's
* aria-describedby so screen readers announce it with the control.
*/
export function VsNfdMark({ id, text }: { id?: string; text: string }): React.JSX.Element {
return (
<span className="vs-nfd-mark" id={id}>
<span aria-hidden="true"> </span>
{text}
</span>
);
}