mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
The binding UI rulebook (card anatomy, spacing scale, text colour, tokens, primitive mapping, enforcement) lived outside the repository, so a contributor could follow a code comment to a file they could not open. It now lives at docs/contributing/ui-standards.md, every comment that cited it points there, and the docs index and ui-guidelines.md say which document wins when they disagree.
177 lines
6.9 KiB
TypeScript
177 lines
6.9 KiB
TypeScript
/**
|
|
* What a browser has to prove about v1.37 sharing, enumerated.
|
|
*
|
|
* Two lists, and both exist for the same reason: a browser suite reports what
|
|
* it ran, never what it did not. A state nobody wrote a test for and a journey
|
|
* somebody quietly dropped both look like a green run. Naming them in one
|
|
* place that a fast test reads back turns "we cover the error state" from a
|
|
* claim in a summary into something that fails.
|
|
*
|
|
* Nothing here drives a browser. The enumeration is the contract; the spec is
|
|
* the implementation, and `src/__tests__/sharing-accessibility-states.test.ts`
|
|
* is what holds the two together.
|
|
*/
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* The five states */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Every state a shared-record surface can be in, and all five have to be
|
|
* usable — not merely reachable.
|
|
*
|
|
* Accessibility work concentrates on the state where everything worked,
|
|
* because that is the state somebody building the screen is looking at. The
|
|
* other four are where a person who cannot see the screen is most stranded: a
|
|
* spinner nobody announces, an error card that reads as an empty page, a
|
|
* refusal whose only way out is a button no keyboard reaches.
|
|
*/
|
|
export const ACCESSIBILITY_STATES = [
|
|
"success",
|
|
"empty",
|
|
"loading",
|
|
"error",
|
|
"refusal",
|
|
] as const;
|
|
|
|
export type AccessibilityState = (typeof ACCESSIBILITY_STATES)[number];
|
|
|
|
export interface AccessibilityStateCase {
|
|
readonly state: AccessibilityState;
|
|
/**
|
|
* The `data-slot` that proves the state is actually on screen.
|
|
*
|
|
* A scan of a page that never entered the state passes trivially, so every
|
|
* case names the element that says it arrived, and the fast guard checks the
|
|
* component tree really renders that slot.
|
|
*/
|
|
readonly anchor: string;
|
|
/** The module that renders {@link anchor}, so a state cannot be invented. */
|
|
readonly rendersIn: string;
|
|
/**
|
|
* The control keyboard focus must reach, or `null` where the state offers
|
|
* none at all. A loading state has nothing to press; every other state here
|
|
* has a way forward, and a way forward a keyboard cannot reach is not one.
|
|
*/
|
|
readonly keyboardTarget: string | null;
|
|
/**
|
|
* Copy the person has to read and act on, which must render as content
|
|
* rather than as muted meta (ui-standards.md §3). `null` where the state's only
|
|
* text is a screen-reader label, which has no visual contrast to check.
|
|
*/
|
|
readonly contrastTarget: string | null;
|
|
/** Why this state is on the list, in one line. */
|
|
readonly claim: string;
|
|
}
|
|
|
|
export const SHARING_ACCESSIBILITY_STATES = [
|
|
{
|
|
state: "success",
|
|
anchor: '[data-slot="profile-summary"]',
|
|
rendersIn: "src/components/records/profile-summary.tsx",
|
|
keyboardTarget: '[data-slot="shared-record-banner-exit"]',
|
|
contrastTarget: '[data-slot="shared-record-banner-context"]',
|
|
claim:
|
|
"inside somebody else's record, the line saying whose it is and the way out are both usable",
|
|
},
|
|
{
|
|
state: "empty",
|
|
anchor: '[data-slot="grants-given-card"] [data-slot="empty-state"]',
|
|
rendersIn: "src/components/ui/empty-state.tsx",
|
|
keyboardTarget: '[data-slot="grant-invite-identifier"]',
|
|
contrastTarget:
|
|
'[data-slot="grants-given-card"] [data-slot="empty-state"] p',
|
|
claim:
|
|
"nothing shared yet reads as an answer rather than as a page that failed to load",
|
|
},
|
|
{
|
|
state: "loading",
|
|
anchor: '[data-slot="record-scope-hydration-gate"]',
|
|
rendersIn: "src/components/layout/record-scope-hydration-gate.tsx",
|
|
keyboardTarget: null,
|
|
contrastTarget: null,
|
|
claim:
|
|
"the hold before the active record is known is announced, not a silent blank",
|
|
},
|
|
{
|
|
state: "error",
|
|
anchor: '[data-slot="query-error-card"]',
|
|
rendersIn: "src/components/ui/query-error-card.tsx",
|
|
keyboardTarget: '[data-slot="query-error-retry"]',
|
|
contrastTarget: '[data-slot="query-error-card"] p',
|
|
claim: "a failed read says so and offers the retry to a keyboard",
|
|
},
|
|
{
|
|
state: "refusal",
|
|
anchor: '[data-slot="shared-record-unavailable"]',
|
|
rendersIn: "src/components/layout/shared-record-unavailable.tsx",
|
|
keyboardTarget: '[data-slot="shared-record-unavailable-leave"]',
|
|
contrastTarget: '[data-slot="shared-record-unavailable"] p',
|
|
claim:
|
|
"a surface sharing does not cover explains itself and keeps a way back to one's own record",
|
|
},
|
|
] as const satisfies readonly AccessibilityStateCase[];
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* The eight journeys */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
export interface JourneyClaim {
|
|
/** Stable id, quoted in the spec's test title so the two can be matched. */
|
|
readonly id: string;
|
|
/** What the journey proves, in the release's own words. */
|
|
readonly claim: string;
|
|
}
|
|
|
|
/**
|
|
* The eight end-to-end proofs the release is required to run, transcribed from
|
|
* the phase context rather than restated: a paraphrase is where a requirement
|
|
* quietly narrows.
|
|
*
|
|
* The list is here rather than in a spec file because more than one spec runs
|
|
* parts of it, and because the integration plan reconciles the whole set at
|
|
* the end. An id that no test title mentions is a journey nobody ran.
|
|
*/
|
|
export const V137_E2E_JOURNEYS = [
|
|
{
|
|
id: "J1-adult-levels-lifecycle",
|
|
claim:
|
|
"Adult full READ, scoped READ, WRITE and MANAGE through invite, accept, switch, expiry, revoke and direct URL.",
|
|
},
|
|
{
|
|
id: "J2-manage-edits-without-settings",
|
|
claim:
|
|
"Adult MANAGE edits and deletes with specific activity, and zero Guardian-only settings or integration reach.",
|
|
},
|
|
{
|
|
id: "J3-managed-profile-lifecycle",
|
|
claim:
|
|
"Truthful managed-profile creation, two Guardians, concurrent last-Guardian protection, record settings and deletion.",
|
|
},
|
|
{
|
|
id: "J4-guardian-notification-matrix",
|
|
claim:
|
|
"Two Guardians by two profiles through APNs, Web Push, Telegram and ntfy with recipient locale and preferences, non-interactive payloads and subject/recipient attempts.",
|
|
},
|
|
{
|
|
id: "J5-revoked-idempotent-replay",
|
|
claim:
|
|
"Revoked MANAGE idempotency replay returns 403, no cached DTO and no second mutation.",
|
|
},
|
|
{
|
|
id: "J6-no-delegated-provider-egress",
|
|
claim:
|
|
"An invited MANAGE mutation with a configured provider produces no provider job or call; the owner positive control enqueues.",
|
|
},
|
|
{
|
|
id: "J7-durable-import-actor",
|
|
claim:
|
|
"Asynchronous import completion retains record, actor and job identity.",
|
|
},
|
|
{
|
|
id: "J8-old-client-compatibility",
|
|
claim:
|
|
"Old-client compatibility preserves legacy decode, scoped fail-closed behaviour and MANAGE decode until the canonical client is ready.",
|
|
},
|
|
] as const satisfies readonly JourneyClaim[];
|