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

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:
ekklesia
2026-10-16 19:14:46 -05:00
co-authored by Claude Opus 5.5
parent d9dafcb0f7
commit 0009f62fbd
10 changed files with 261 additions and 97 deletions
@@ -2,6 +2,7 @@
- Status: Accepted
- Date: 2026-10-15
- Teal accent and teal-tinted dark palette superseded by: [0066](0066-default-purple-accent.md)
- Extends: [0039](0039-shared-design-tokens-and-elements.md),
[0040](0040-module-stylesheets-and-artifact-isolation.md),
[0058](0058-dashboard-shell-navigation-and-lucide-icons.md),
@@ -147,11 +147,17 @@ and a `role="listbox"` of options).
### The normal list shows active displays, current as they change
The Paired displays list shows **active** displays only. A revoked display leaves it at once — the
page asks for the list again right after revoking — and the page says so, with focus on that notice
because the display's own card is gone. The revoked device row and its permission rows stay in the
The Paired displays list shows **active** displays only. A removed display leaves it at once — the
page asks for the list again right after removing it — and the page says so, with focus on that
notice because the display's own card is gone. The device row and its permission rows stay in the
database, marked revoked, as security history; a history view can show them later.
**Administrators remove a display; the platform revokes its credential.** The interface says
*Remove*, *Remove display?*, and *Display removed*, and never shows "revoked". The domain does not
change for presentation: the device and its permissions are revoked, the endpoint is still
`POST /api/v1/devices/:id/revoke`, the fields are still `revoked`/`revokedAt`, and security and audit
history still describe a credential revocation.
A newly paired display appears without a reload. Approval does not create the display: the screen
does, when it collects its credential on its next poll, a few seconds later (ADR 0036). So after
approving, the page shows a "Waiting for the screen to connect…" placeholder and asks for the list
@@ -166,7 +172,7 @@ Each active display shows:
purpose: last-seen is recorded at most every 15 minutes;
- **Showing:** a module, or *Nothing yet*;
- last seen and paired dates;
- **Change** and **Revoke**.
- **Change** and **Remove**.
### Changing what a display shows: one target, the active display permission
@@ -0,0 +1,112 @@
# 0066: The default accent is a deep purple anchored on `#210050`
- Status: Accepted
- Date: 2026-10-16
- Supersedes, in part: [0060](0060-light-dark-system-theme-and-motion.md) (the teal accent and the
teal-tinted charcoal of its designed dark theme)
- Extends: [0039](0039-shared-design-tokens-and-elements.md),
[0060](0060-light-dark-system-theme-and-motion.md)
## Context
Ekklesia's default theme was accented in teal. The light primary action was a pale teal (`#4fb3a8`)
with dark text on it. ADR 0060's designed dark theme described "teal-tinted charcoal" backgrounds and
"the same teal accent, slightly lifted". The neutrals carried the same teal tint, so did the
selection and information washes, and the whole product read as teal.
The maintainer chose a purple default anchored on `#210050`. Branding settings will later let an
administrator choose the accent. This decision changes only the **default**.
A single hex cannot serve every place the accent is used. `#210050` is very dark: it is 17.8:1
against white, but about 1.2:1 against a dark-theme page, where it would all but vanish.
## Decision
### The accent is set through the semantic tokens only
The change is made in `@ekklesia/design-tokens`' registry, and nowhere else. No Dashboard component
names a purple. The shared `@ekklesia/ui` elements and the shell already draw from `accent.*`,
`state.selected-*`, and `status.info-*`, so they inherit the new values. Modules read the same
`--ekklesia-*` custom properties (ADR 0040), so their token inheritance is unchanged.
### `#210050` is the light primary action; every other value is a variant on its hue
Each variant keeps the anchor's OKLCH hue (≈291.5°). Lightness and chroma are chosen for contrast
where the variant is used.
| Token | Light | Dark | Used for |
| --- | --- | --- | --- |
| `accent.base` | **`#210050`** | `#ac9bf6` | primary button, tab underline, spinner, brand mark |
| `accent.hover` | `#340e71` | `#bbacff` | hovered primary button |
| `accent.active` | `#120033` | `#9a88e2` | pressed primary button |
| `accent.foreground` | `#ffffff` | **`#210050`** | text on the accent |
| `accent.strong` | `#4d2a99` | `#c8bcff` | accent text and icons; checked box and radio |
| `accent.subtle` | `#f0eeff` | `#2e2849` | selected tab wash |
| `accent.focus` | `#6a40c9` | `#bfb0ff` | focus ring |
| `state.selected-background` | `#eceaff` | `#342b53` | the open page in navigation, selected option |
| `state.selected-foreground` | `#48278e` | `#d7d0ff` | its label |
| `status.info-background` | `#f1f0ff` | `#292440` | information notice and badge |
| `status.info-border` | `#c7c0f2` | `#564b83` | its border |
**The light theme turns around.** A pale accent with dark text becomes the deep anchor with white
text: 17.8:1, or 14.5:1 hovered.
**Light accent text and the focus ring stay off the anchor itself.** In body copy `#210050` is
almost indistinguishable from near-black text, so accent text and icons use a brighter `#4d2a99`
(10.0:1 on a surface). The focus ring uses a vivid `#6a40c9` (6.1:1 on the page). Next to a dark
primary button, a ring in the anchor's own colour would merge into the button; this one is clearly
distinct.
**Dark keeps ADR 0060's principle: lifted, not glowing.** The accent is a lavender (`#ac9bf6`). The
anchor is drawn on it as its foreground, at 7.4:1, and at 5.9:1 pressed. Selected and information
washes are deep purple tints, never saturated fills.
### The neutrals lose their teal tint
Every teal-tinted neutral moves to the anchor's hue at the same OKLab lightness and chroma. This
covers the backgrounds, text, borders, hover and pressed washes, the placeholder, the shadows'
colour, and the dark overlay.
- **Light:** a faint violet-grey (page `#f6f5f9`, text `#201f28`).
- **Dark:** a violet-grey charcoal (page `#141319`, text `#eaeaf1`).
ADR 0060's other rules still hold: never pure black, never pure white, and an elevation ladder.
Because lightness is kept, contrast is kept. The tightest pairings move by less than 0.1. For
example, `border.control` on the light page goes from 3.24 to 3.28, and `text.muted` on the light
page from 5.04 to 5.10. Success, warning, and danger keep their own hues.
### Contrast stays tested
`packages/design-tokens/tests/contrast.test.ts` keeps all 41 pairings it already checked, in both
schemes. It adds six pairings this accent depends on:
- the accent as a non-text indicator on the page, the canvas, and a surface;
- the selected tab's label on its wash;
- accent text in a dialog;
- the mark inside a checked box or radio.
A test also pins the anchor: `accent.base` is `#210050` with white on it in light, and in dark it is
lifted, with the anchor as its foreground.
## Alternatives
- **The same hex everywhere.** Rejected: `#210050` fails as an indicator on dark surfaces at about
1.2:1. As light text, it is too close to the body copy to read as an accent.
- **Purple accent tokens, teal-tinted neutrals.** Rejected: the teal stays visible in every surface
and border, most of all in dark, and clashes with the accent.
- **Hue-rotating every accent token at its old lightness and chroma.** Rejected for the accent
tokens: teal's low chroma gives a dull grey-purple. It is used for the neutrals, where a faint
tint is the point.
- **Waiting for Branding settings.** Rejected by the maintainer: the default should be right before
anyone configures it.
## Consequences
- Every first-party surface turns purple from tokens alone: the Dashboard, the sign-in pages, and
the shared elements. The display host stays light (ADR 0060) and takes the light values.
- Modules that use the shared tokens, such as Signage, follow the new default with no change of
their own. A module that hard-coded the old teal would not, which is why no module should.
- A deployment that overrides tokens at its host root is unaffected.
- When Branding settings arrive, the accent becomes an administrator's choice. That feature must
derive the same set of variants and pass the same contrast tests. This decision is its default.
+2 -1
View File
@@ -68,9 +68,10 @@ ADRs capture durable choices that affect more than one package or phase.
- [0057: Dashboard URLs are real, host-owned routes](0057-dashboard-urls-are-host-owned-routes.md) — Accepted
- [0058: The Dashboard shell's navigation, and Lucide as Ekklesia's icon vocabulary](0058-dashboard-shell-navigation-and-lucide-icons.md) — Accepted (device panel and phone menu placement superseded by 0064)
- [0059: Shared interaction primitives — confirmation, toasts, errors, loading, Back, and no native dialogs](0059-shared-interaction-primitives.md) — Accepted
- [0060: Light, Dark, and System appearance; reduced motion; and one radius scale](0060-light-dark-system-theme-and-motion.md) — Accepted
- [0060: Light, Dark, and System appearance; reduced motion; and one radius scale](0060-light-dark-system-theme-and-motion.md) — Accepted (teal accent and palette superseded by 0066)
- [0061: Settings is one host-owned hierarchy; module lifecycle is Discover, and a module's settings live at `/settings/<module-id>`](0061-settings-hierarchy-and-module-settings-routes.md) — Accepted (Discover's address and the landing page superseded by 0064)
- [0062: The account area, and appearance preferences that belong to the account](0062-account-area-and-synced-appearance-preferences.md) — Accepted
- [0063: Module navigation — manifest v9 icons and pages, real module routes, and return-to-origin](0063-module-navigation-contract-manifest-v9.md) — Accepted
- [0064: Discover is its own page, the sidebar is navigation only, Settings opens on an overview, and phones get a sticky header](0064-stage-1-shell-review-corrections.md) — Accepted
- [0065: Displays pair at the platform's own `/display/setup`; phone forms never zoom; the drawer locks the page](0065-display-setup-phone-forms-and-drawer-scroll-lock.md) — Accepted
- [0066: The default accent is a deep purple anchored on `#210050`](0066-default-purple-accent.md) — Accepted