Say Remove for displays, and make the default accent purple (#210050)
CI / verify (pull_request) Successful in 1m14s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 49s
CI / migration-smoke (pull_request) Successful in 2m30s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 45s
CI / package-publication (pull_request) Successful in 56s
CI / deploy-smoke (split) (pull_request) Successful in 8m55s
CI / verify (pull_request) Successful in 1m14s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 49s
CI / migration-smoke (pull_request) Successful in 2m30s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 45s
CI / package-publication (pull_request) Successful in 56s
CI / deploy-smoke (split) (pull_request) Successful in 8m55s
Administrators now remove a display: the card's Remove, "Remove display?", and "Display removed". The platform still revokes the device's credential; the endpoint, fields, and security history keep revoke/revoked (ADR 0065). The default accent is a deep purple anchored on #210050, set in the shared design tokens only (ADR 0066, superseding ADR 0060's teal in part). The anchor is the light primary action with white on it; the light accent text, focus ring, and selection, and the dark theme's lavender accent, are variants on its hue chosen for contrast. The teal-tinted neutrals move to a faint violet-grey at the same lightness, so every contrast pairing holds. The contrast test gains six pairings the accent depends on, and pins the anchor. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d9dafcb0f7
commit
0009f62fbd
@@ -419,7 +419,8 @@ function DisplaySetupPanel(props: {
|
||||
/**
|
||||
* One active display: its one name, whether it is online (coarsely — see {@link ONLINE_WITHIN_MS}),
|
||||
* what it shows now, and the two things an administrator does with it — change what it shows, and
|
||||
* revoke it. Revoked displays are not listed: they are history, not something to manage.
|
||||
* remove it. Removing a display revokes its credential; revoked displays are not listed: they are
|
||||
* history, not something to manage. The interface says "remove", the platform's domain "revoke".
|
||||
*/
|
||||
function DisplayCard(props: {
|
||||
readonly device: HostDeviceSummary;
|
||||
@@ -470,7 +471,7 @@ function DisplayCard(props: {
|
||||
</Button>
|
||||
)}
|
||||
<Button data-testid={`device-revoke-${device.id}`} onClick={props.onRevoke} size="sm">
|
||||
Revoke
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
@@ -765,9 +766,9 @@ export function DisplaysSettingsPage(props: DisplaysSettingsPageProps): ReactEle
|
||||
</span>
|
||||
</InlineNotice>
|
||||
) : (
|
||||
<InlineNotice title="Display revoked" tone="info">
|
||||
<InlineNotice title="Display removed" tone="info">
|
||||
<span data-testid="device-revoke-success">
|
||||
{outcome.name} is revoked. Its screen returns to its pairing code.
|
||||
{outcome.name} was removed. Its screen returns to its pairing code.
|
||||
</span>
|
||||
</InlineNotice>
|
||||
)}
|
||||
@@ -787,7 +788,7 @@ export function DisplaysSettingsPage(props: DisplaysSettingsPageProps): ReactEle
|
||||
{list.kind === "loading" ? <LoadingState label="Loading displays…" /> : undefined}
|
||||
{list.kind === "denied" ? (
|
||||
<EmptyState
|
||||
description="If you need to pair or revoke displays, ask an administrator."
|
||||
description="If you need to add or remove displays, ask an administrator."
|
||||
title="Displays aren't available to you"
|
||||
/>
|
||||
) : undefined}
|
||||
@@ -871,7 +872,7 @@ export function DisplaysSettingsPage(props: DisplaysSettingsPageProps): ReactEle
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Revoking strands the screen until someone re-pairs it in person, so the consequence is
|
||||
{/* Removing (revoking its credential) strands the screen until someone re-pairs it in person, so the consequence is
|
||||
spelled out before it happens — never a browser-native confirm. */}
|
||||
<Dialog
|
||||
actions={
|
||||
@@ -889,17 +890,18 @@ export function DisplaysSettingsPage(props: DisplaysSettingsPageProps): ReactEle
|
||||
onClick={() => (confirming ? void revoke(confirming) : undefined)}
|
||||
variant="danger"
|
||||
>
|
||||
Revoke display
|
||||
Remove display
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
onDismiss={dismissConfirmation}
|
||||
open={confirming !== undefined}
|
||||
title={`Revoke ${confirming?.name ?? "this display"}?`}
|
||||
title="Remove display?"
|
||||
>
|
||||
<p style={{ margin: 0 }}>
|
||||
The screen stops showing content within a few seconds and returns to its pairing code.
|
||||
This cannot be undone: to use it again, pair it from the screen with a new code.
|
||||
{confirming?.name ?? "This display"} stops showing content within a few seconds and its
|
||||
screen returns to its pairing code. This cannot be undone: to use it again, pair it from
|
||||
the screen with a new code.
|
||||
</p>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -259,7 +259,7 @@ describe("Settings → Displays", () => {
|
||||
expect(required("device-device-1-showing").textContent).toBe("Showing: Nothing yet");
|
||||
expect(card.textContent).toContain("Last seen within the last hour");
|
||||
expect(query("device-change-device-1")?.textContent).toBe("Change");
|
||||
expect(query("device-revoke-device-1")?.textContent).toBe("Revoke");
|
||||
expect(query("device-revoke-device-1")?.textContent).toBe("Remove");
|
||||
expect(container.querySelector("h1")?.textContent).toBe("Displays");
|
||||
// Setting up a new screen comes first, in one place, at the platform's own setup address.
|
||||
const setup = required("display-setup");
|
||||
@@ -271,7 +271,7 @@ describe("Settings → Displays", () => {
|
||||
expect(document.activeElement).toBe(before);
|
||||
});
|
||||
|
||||
it("shows only active displays: revoked ones are history, not the normal list", async () => {
|
||||
it("shows only active displays: removed ones are history, not the normal list", async () => {
|
||||
await renderPage({
|
||||
devices: [
|
||||
{
|
||||
@@ -300,7 +300,8 @@ describe("Settings → Displays", () => {
|
||||
expect(query("device-a")).not.toBeNull();
|
||||
expect(query("device-b")).toBeNull();
|
||||
expect(query("device-c")).toBeNull();
|
||||
expect(container.textContent).not.toContain("Revoked");
|
||||
// Removed displays are never described as "revoked" in the normal interface.
|
||||
expect(container.textContent).not.toMatch(/revok/iu);
|
||||
expect(required("device-a").textContent).toContain("Not connected yet");
|
||||
});
|
||||
|
||||
@@ -503,18 +504,22 @@ describe("changing what a display shows (ADR 0065)", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Revoking a display strands that screen until someone re-pairs it in person, so the page explains
|
||||
* the consequence in its own dialog first — never with a browser-native confirm (DESIGN §21).
|
||||
* Removing a display (revoking its credential) strands that screen until someone re-pairs it in
|
||||
* person, so the page explains the consequence in its own dialog first — never with a
|
||||
* browser-native confirm (DESIGN §21). The interface says "remove"; only the platform says "revoke".
|
||||
*/
|
||||
describe("display revocation", () => {
|
||||
it("asks in a product dialog before revoking, and does nothing on cancel", async () => {
|
||||
describe("removing a display", () => {
|
||||
it("asks in a product dialog before removing, and does nothing on cancel", async () => {
|
||||
const confirm = vi.fn(() => true);
|
||||
Object.assign(window, { confirm });
|
||||
const platform = await renderPage();
|
||||
|
||||
await click("device-revoke-device-1");
|
||||
const dialog = container.querySelector('[role="dialog"]');
|
||||
expect(dialog?.textContent).toContain("Revoke Foyer TV?");
|
||||
// Administrators remove a display; revoking its credential is the platform's word, not theirs.
|
||||
expect(dialog?.textContent).toContain("Remove display?");
|
||||
expect(dialog?.textContent).toContain("Foyer TV stops showing content");
|
||||
expect(dialog?.textContent).not.toMatch(/revok/iu);
|
||||
expect(dialog?.textContent).toContain("pairing code");
|
||||
|
||||
await click("device-revoke-cancel");
|
||||
@@ -523,7 +528,7 @@ describe("display revocation", () => {
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes a revoked display from the list at once, and says what happened", async () => {
|
||||
it("takes a removed display off the list at once, and says what happened", async () => {
|
||||
const platform = await renderPage();
|
||||
|
||||
await click("device-revoke-device-1");
|
||||
@@ -536,7 +541,9 @@ describe("display revocation", () => {
|
||||
expect(query("device-device-1")).toBeNull();
|
||||
expect(platform.devices[0]?.revoked).toBe(true);
|
||||
expect(required("device-list-empty")).not.toBeNull();
|
||||
expect(required("device-revoke-success").textContent).toContain("Foyer TV is revoked");
|
||||
expect(required("device-outcome").textContent).toContain("Display removed");
|
||||
expect(required("device-revoke-success").textContent).toContain("Foyer TV was removed");
|
||||
expect(container.textContent).not.toMatch(/revok/iu);
|
||||
// Its card is gone, so focus lands on what happened rather than on the page.
|
||||
expect(document.activeElement).toBe(required("device-outcome"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user