feat: add shared versioned Hope palette tokens
This commit is contained in:
1 parent
437745bcca
commit
985c812a75
6 files changed
+1663
No files matched your search
@@ -0,0 +1,68 @@
|
||||
# Hope shared themes · v1
|
||||
|
||||
## Identity and scope
|
||||
|
||||
Hope is an authenticated voice companion, not a model dashboard. Users may talk in a dim room or review text in daylight; appearance therefore follows the device by default, with explicit light/dark overrides. The restrained Nearlight system uses opaque tinted surfaces, readable ink and one action accent. **Opal Pink remains the default**, preserving the existing iOS identity rather than the historical web navy palette.
|
||||
|
||||
The source of truth is [hope-palette.v1.json](./hope-palette.v1.json), schema version `1`, token version `1.0.0`. Palette IDs are stable persisted/API values: `opal` (display label **Opal Pink**), `graphite`, `ocean`, `sage`, `lavender`. Appearance IDs are `system`, `light`, `dark`; default `system`. Labels are presentation, never storage keys. Theme settings must not affect authentication, voice, conversation or error state. Consumers should normalize unknown IDs to Opal/system.
|
||||
|
||||
The extracted native source is [Nearlight.swift](../../voice-ios/Sources/App/Nearlight.swift), lines 11–43. Opal's exact ground/surface/personaSurface/ink/quiet/rosewood/onRosewood values are preserved. Alert channels are deliberately stored as normalized decimal sRGB `[0.675, 0.208, 0.282]` and `[1.0, 0.604, 0.675]`: rounding them to hex would change the existing UIColor values. All other opaque primitives use six-digit sRGB hex. The generator emits round-trip precision channel values, not an approximate color conversion.
|
||||
|
||||
## Architecture and outputs
|
||||
|
||||
1. **Primitive values**: explicit sRGB palette values and shared alert primitives.
|
||||
2. **Semantic roles**: `ground`, `surface`, `personaSurface`, `ink`, `quiet`, `accent`, `onAccent`, `alert`, `onAlert`, `focus`, `controlBorder`; aliases are documented in the JSON. `accent` replaces color-specific naming for new code; existing `rosewood` semantics remain a compatibility alias.
|
||||
3. **Component aliases**: action, input, persona and error roles refer to semantic roles, never raw values.
|
||||
|
||||
[generate_hope_themes.py](../scripts/generate_hope_themes.py) uses the standard library only. It validates required roles and contrast **before writing**, generates deterministic artifacts, and embeds source version/SHA256. Change the source and regenerate; do not hand-edit outputs. A changed schema requires a new schema version; removed/repurposed role or palette IDs require a major token version. Additive roles/palettes use a minor version; compatible corrections use a patch version. Native identity changes require explicit product approval.
|
||||
|
||||
### Web handoff
|
||||
|
||||
Load [hope-themes.css](../web/hope-themes.css) after existing base styles, then consume prefixed variables:
|
||||
|
||||
```html
|
||||
<html data-palette="opal" data-appearance="system">
|
||||
```
|
||||
|
||||
```css
|
||||
body { background: var(--hope-ground); color: var(--hope-ink); }
|
||||
button.primary { background: var(--hope-action-background); color: var(--hope-action-foreground); }
|
||||
input { background: var(--hope-input-background); color: var(--hope-input-foreground); border-color: var(--hope-input-border); }
|
||||
input::placeholder { color: var(--hope-input-placeholder); opacity: 1; }
|
||||
:focus-visible { outline: 2px solid var(--hope-focus); outline-offset: 3px; }
|
||||
```
|
||||
|
||||
Attributes belong on the document root, not arbitrary descendants. Missing/stale palette IDs inherit Opal. Missing/stale/system appearance follows `prefers-color-scheme`; explicit light/dark overrides the OS. `color-scheme` matches the effective appearance so native controls match. Theme styles define tokens only; they do not modify layout, attach event listeners, store preferences, or override legacy unprefixed CSS automatically. Consumers own preference loading/persistence and loading the sheet before rendering to avoid a theme flash.
|
||||
|
||||
### iOS handoff
|
||||
|
||||
[HopeTheme.generated.swift](../generated/HopeTheme.generated.swift) is staged **in this backend repository**, not installed into the iOS target. The iOS teammate may copy the unchanged artifact into the app build and map existing Nearlight roles:
|
||||
|
||||
```swift
|
||||
HopeTheme.color(.ground, palette: .opal, appearance: .system)
|
||||
HopeTheme.color(.accent, palette: palette, appearance: appearance) // Nearlight.rosewood
|
||||
HopeTheme.color(.onAccent, palette: palette, appearance: appearance) // Nearlight.onRosewood
|
||||
```
|
||||
|
||||
`HopePalette` and `HopeAppearance` are raw-string enums for persistence. `HopePalette.label` supplies display labels; `HopeTheme.version` identifies the source version. System colors use the native trait-aware UIColor provider; explicit overrides return a fixed color. UIKit/SwiftUI are required. Spacing, typography, radii, motion and native materials remain the responsibility of the existing Nearlight system; this task does not alter them.
|
||||
|
||||
## Accessibility and state rules
|
||||
|
||||
- Primary/supporting/placeholder/accent/error text is tested at **≥4.5:1** on ground, surface and persona surfaces for every palette and appearance. Action/destructive labels also meet 4.5:1; focus and essential control boundaries meet **≥3:1** against those surfaces.
|
||||
- All 230 declared foreground/background checks pass. Lowest normal-text contrast is **4.558:1** (Sage light `quiet` on `selectedWash`). Checks use WCAG relative luminance and sRGB transfer functions. Run `--contrast` for the complete measured report. This verifies declared token pairings, **not** every rendered UI, arbitrary layering or hardware display.
|
||||
- `separator` is decorative only. Native's `ink × 14%`, `accent × 10%` selected wash and `alert × 8%` error wash are precomposited over `surface` into opaque shared tokens. On that surface they preserve the original appearance. Do not reuse the baked wash expecting translucency over a different parent; use a reviewed derivation if needed.
|
||||
- Essential boundaries use `controlBorder`, not the low-contrast separator. A focus ring needs thickness and offset, not color alone. In forced-colors mode consumers must retain browser/system focus and control boundaries.
|
||||
- **Errors are not represented only by palette/color**: keep explicit messages, icons or labels, accessibility announcements and recovery actions. `alert` is shared across palettes, not the theme's accent. Selected controls also need checkmarks/pressed or selected semantics. Never use color alone for listening, speaking, offline, loading, success or destructive state.
|
||||
- Disabled controls may use native disabled semantics; do not dim readable supporting text into a disabled color. Consumers retain reduced-motion handling and Dynamic Type/text zoom support.
|
||||
|
||||
## Offline verification (no NAS, no deployment)
|
||||
|
||||
```sh
|
||||
python3 scripts/generate_hope_themes.py
|
||||
python3 scripts/generate_hope_themes.py --check
|
||||
python3 scripts/generate_hope_themes.py --check --contrast
|
||||
python3 -m unittest discover -s tests/unit -p test_hope_themes.py -v
|
||||
swiftc -frontend -parse generated/HopeTheme.generated.swift
|
||||
```
|
||||
|
||||
[Token tests](../tests/unit/test_hope_themes.py) fix the native identity, ensure shared alert semantics and opaque derivations, reject malformed primitives/missing roles/low contrast, verify deterministic artifact drift, and confirm round-trip Swift channels. The optional Playwright test runs local Chromium against in-memory HTML/CSS with **70 palette/appearance/OS combinations**, including missing/stale values and overrides; it does not call an API or server. It skips if the Playwright module is absent; install Playwright and its Chromium browser for full verification. Swift parsing verifies syntax only; native UIKit compilation and final device rendering belong to the iOS integration qualification. Do not run the legacy top-level live-default test suite for token changes.
|
||||
Reference in new issue
Block a user