Add curated semantic theme system with live preview #3

Open
opened 2026-09-01 13:28:36 +00:00 by kayg · 0 comments
Owner

Summary

Add a curated built-in theme system for macOS, with shared semantic roles that can later map to iOS without forcing identical geometry or materials. The goal is a small set of intentionally art-directed themes—not a color-soup palette editor—and a live, reversible preview in Appearance settings.

The reader redesign (top padding below the physical-top fade, stronger subject contrast, distinct sender/recipient/header, subject, and body regions, expandable full headers) is a separate concern. This issue must provide reader-region roles, but must not block that work or choose its final palettes.

Current seams to build on

  • App/Sources/Support/AppearanceSettings.swift is the @MainActor @Observable owner of persisted appearance choices. It currently owns AppearanceMode (system, light, dark), window opacity, the blur/Liquid Glass treatment choice, message-list row density, and the AccentSource.
  • AccentSource is the current canonical system/custom accent bridge. SwiftUI receives it through the environment and .tint; AppKit message-list cells resolve the same NSColor. Existing semantic warning/error/content colors are intentionally outside that one-accent interface.
  • App/Sources/Shell/WindowBackdrop.swift already centralizes the blur/glass/opaque decision in WindowBackdropPlan. Reduce Transparency and fullscreen force an opaque fallback; opacity and the treatment choice are persisted separately.
  • App/Sources/Shell/MainWindowController.swift injects appearance into the main shell, while App/Sources/Account/SettingsWindow.swift is the existing Appearance settings surface. Any new theme selection should remain observable across both windows without requiring a reopen.
  • App/Sources/MessageWeb/MessageWebView.swift wraps sanitized HTML and injects reader CSS. Its current boundary intentionally leaves author colors in the email HTML alone and blocks network loads except approved internal/data-image paths. App/Sources/Viewer/MessageViewer.swift owns native reader chrome around the web content.

Use these seams rather than adding per-view defaults or a second accent source. Keep platform rendering at the edge: the semantic theme value should be shared, while SwiftUI, AppKit, and WebKit adapters choose their platform-appropriate representation.

Proposed architecture

  1. Introduce a small, value-type semantic theme model (for example ThemeID, ThemeVariant, ThemeTokens, and a resolver) beside AppearanceSettings. A token must describe a role, not a component-specific arbitrary color.
  2. Define a curated preset catalog with stable IDs, design rationale, and explicit light/dark variants. AppearanceSettings persists only a stable preset ID plus the existing appearance mode/variant choice and schema version; it does not persist an unbounded bag of RGB values.
  3. Resolve system through the effective macOS color scheme. A user-selected light or dark mode must select the corresponding variant of the chosen preset. Unknown IDs, malformed persisted values, and future schema versions must fall back safely to the default preset without crashing or silently creating a new palette.
  4. Make the resolved token set observable and inject it through the existing shell/settings environment. AppKit bridges (message-list rows, separators, window chrome) and the reader host should consume the same roles rather than independently deriving colors.
  5. Keep the existing custom accent compatible during migration: either map it explicitly to the selection/accent roles or provide a one-time, documented fallback. Reset must remove stale custom theme state in a predictable way. Do not create a second persistence key that can disagree with AccentSource.
  6. Model treatment policy separately from color tokens. A theme may express preferred dim/tint/material roles, but WindowBackdropPlan remains the enforcement point for Reduce Transparency, fullscreen, and opaque fallback. Native materials are optional implementation details, not a requirement of a preset.

Semantic token families

The initial role inventory should cover at least:

  • Window and shell: window canvas, elevated window surface, toolbar/titlebar surface, overlay/popover surface, dimming veil, and tint wash.
  • Sidebar: sidebar canvas, sidebar text hierarchy, selected row, hover row, disclosure/icon treatment, and unread badge foreground/background.
  • Message list: list canvas, row surface, hover row, selected row, unread emphasis, preview text, date/meta text, focus ring, and separators.
  • Reader regions: reader canvas plus distinct readerSubjectTint, readerHeaderTint, and readerBodyTint roles. Also provide reader subject/header/body text hierarchy and separators so the native reader chrome can make those regions visually distinct without blur, glass, or transparency. These are semantic roles only; final palette choices belong to the theme design and remain independent of the reader layout redesign.
  • Text and content: primary, secondary, tertiary, disabled, inverted, link, visited-link (if needed), and placeholder text. Ensure hierarchy is not conveyed by color alone.
  • Interaction: accent, selection fill, selected-content text, focus ring, keyboard-focus fill, hover, pressed, and disabled control states. Selection and focus must remain distinguishable in increased contrast.
  • Structure and status: separator, strong separator, border, warning, error, success, quarantine, find-highlight, and attachment/status treatments. Status roles must not rely on hue alone.
  • Material policy: preferred blur/glass/opaque treatment, maximum dimming, tint strength, and a high-contrast/Reduce Transparency fallback policy. These policy values must be bounded and resolved, not arbitrary user-entered effects.

The resolver should be able to return a contrast-adjusted set for macOS Increased Contrast and a fully opaque set when Reduce Transparency is enabled. It must never use dimming or translucency to make primary text fail contrast.

Presets

Ship several named families with an intentional visual thesis and hand-authored light/dark pairs. For example:

  • Paper & Ink: warm paper surfaces, ink-like text hierarchy, restrained indigo interaction color, and quiet separators; the dark pair becomes charcoal/ink rather than merely inverting every value.
  • Midnight Signal: deep blue-black shell, cool slate list/reader surfaces, and a focused electric-blue interaction color; the light pair is a cool editorial white with the same information hierarchy.
  • Graphite Workshop: neutral graphite chrome, high-legibility near-white text, and a limited amber signal for interaction/status; the light pair uses mineral gray surfaces and keeps amber reserved for attention.

These names and exact palettes are design decisions to validate, not permission to generate arbitrary combinations. Each preset needs written rationale, tested contrast, and a small preview that demonstrates the shell, sidebar, list, and reader-region roles. Do not add a general-purpose color editor in this work.

Appearance settings behavior

  • Add a preset picker to the existing Appearance section with a compact visual preview and the light/dark/system behavior explained in text.
  • Selecting a preset updates the currently visible main and settings windows live, including AppKit-backed rows and reader chrome. Do not require a restart or window reopen.
  • Provide an explicit Reset action that restores the product default preset and clears the theme override (with existing system/light/dark mode semantics preserved unless the UI clearly says otherwise). Reset must be observable and testable.
  • Keep existing opacity and treatment controls coherent with the new policy. If a preset supplies a preferred treatment, explain whether the user override wins; do not leave two controls that claim ownership of the same state.
  • Persist and migrate deterministically. Cover old AppearanceSettings keys (mode, backdrop/treatment, opacity, message-list lines, and accent) and missing/invalid values. Theme IDs are versioned so a removed preset maps to a documented fallback rather than changing appearance unpredictably.

Reader and HTML isolation

Theme tokens may style Mailternal-owned reader chrome (subject, sender/recipient headers, full-header disclosure, separators, and the subject/header/body region surfaces) and the host fallback canvas. They must not rewrite the author-controlled email design. Scope any WebKit CSS variables/classes to the reader host, keep sanitized HTML in its content boundary, and preserve the current network-blocking and remote-image policy. A malicious message must not be able to inject a theme token, enable a material, load a remote stylesheet, or alter native chrome. Author colors, tables, links, and inline styles must remain isolated from shell/sidebar/list tokens; test both intentionally light and intentionally dark email fixtures.

iOS direction

Define the semantic role names and information hierarchy as the shared contract, not macOS view types, dimensions, or material names. A future iOS renderer should map window/sidebar/list/reader roles to phone-appropriate navigation, sheets, and full-width reader regions. It should preserve the same subject/header/body information principles while using a phone-specific layout and platform-native accessibility behavior. This issue does not implement iOS or require macOS and iOS screenshots to match pixel-for-pixel.

Non-goals

  • No reader production-layout change, top-fade/padding change, header expansion implementation, or final reader palette decision.
  • No arbitrary user-authored palette editor, remote/downloaded themes, per-message themes, or theme scripting.
  • No change to HTML sanitization, remote-image permissions, link handling, or content security policy beyond the isolation hooks needed to consume owned reader roles.
  • No requirement to replace every native system semantic color at once; migrate the listed shell/list/reader roles deliberately and remove duplicate theme paths as each caller moves.
  • No iOS implementation in this ticket.

QA and visual-regression plan

  • Unit-test token resolution for every preset and light/dark/system variant, malformed/unknown persisted IDs, migration from every existing appearance key, reset, Increased Contrast, and Reduce Transparency. Assert bounded opacity/tint values and contrast invariants for primary text, links, controls, selection, and focus.
  • Exercise live preview with both the main window and settings window open; verify SwiftUI and AppKit consumers update without reopening, and reset returns to the documented default.
  • Capture deterministic screenshots for each preset × light/dark variant, then repeat representative shell/list/reader states with Increased Contrast, Reduce Transparency, fullscreen, and narrow/large windows. Review separators, focus, selected/unread rows, status states, and reader region boundaries.
  • Run HTML isolation fixtures containing hostile inline colors, style tags, CSS variables, data images, blocked remote URLs, tables, and long untrusted text. Verify author styling remains content-local, native chrome stays themed, and no new network request is permitted.
  • Add accessibility checks for keyboard focus/selection, VoiceOver labels for preset and reset controls, color-independent status cues, Dynamic Type where applicable, and Reduce Motion/Reduce Transparency interactions. Record golden screenshot baselines and update them only with a reviewed design decision.

Acceptance criteria

  • A curated catalog with at least three named, documented preset families exists, and every family has tested light and dark variants plus a system-resolution path.
  • One observable semantic token model drives window/sidebar/list/reader-owned surfaces, text hierarchy, interaction/selection, separators, status, and bounded dim/tint/material policy; reader subject/header/body tint roles are present.
  • Appearance settings provide a live preset preview and a working reset; changes propagate to SwiftUI and AppKit consumers without restart/reopen.
  • Persistence uses stable versioned IDs, migrates existing appearance/accent state, and safely falls back on invalid or removed values.
  • Reduce Transparency and Increased Contrast produce accessible resolved tokens and preserve an opaque fallback; no primary text or focus state depends on translucent material.
  • Sanitized HTML remains isolated: author colors do not leak into Mailternal chrome, Mailternal theme tokens do not rewrite author content, and the existing network-blocking boundary remains intact.
  • QA evidence covers unit resolution/migration, live preview/reset, accessibility states, visual-regression matrix, and hostile HTML fixtures.
  • Shared semantic roles and information principles are documented so future iOS mapping can be platform-specific without duplicating the macOS palette architecture.

References

## Summary Add a curated built-in theme system for macOS, with shared semantic roles that can later map to iOS without forcing identical geometry or materials. The goal is a small set of intentionally art-directed themes—not a color-soup palette editor—and a live, reversible preview in Appearance settings. The reader redesign (top padding below the physical-top fade, stronger subject contrast, distinct sender/recipient/header, subject, and body regions, expandable full headers) is a separate concern. This issue must provide reader-region roles, but must not block that work or choose its final palettes. ## Current seams to build on - `App/Sources/Support/AppearanceSettings.swift` is the `@MainActor @Observable` owner of persisted appearance choices. It currently owns `AppearanceMode` (`system`, `light`, `dark`), window opacity, the blur/Liquid Glass treatment choice, message-list row density, and the `AccentSource`. - `AccentSource` is the current canonical system/custom accent bridge. SwiftUI receives it through the environment and `.tint`; AppKit message-list cells resolve the same `NSColor`. Existing semantic warning/error/content colors are intentionally outside that one-accent interface. - `App/Sources/Shell/WindowBackdrop.swift` already centralizes the blur/glass/opaque decision in `WindowBackdropPlan`. Reduce Transparency and fullscreen force an opaque fallback; opacity and the treatment choice are persisted separately. - `App/Sources/Shell/MainWindowController.swift` injects appearance into the main shell, while `App/Sources/Account/SettingsWindow.swift` is the existing Appearance settings surface. Any new theme selection should remain observable across both windows without requiring a reopen. - `App/Sources/MessageWeb/MessageWebView.swift` wraps sanitized HTML and injects reader CSS. Its current boundary intentionally leaves author colors in the email HTML alone and blocks network loads except approved internal/data-image paths. `App/Sources/Viewer/MessageViewer.swift` owns native reader chrome around the web content. Use these seams rather than adding per-view defaults or a second accent source. Keep platform rendering at the edge: the semantic theme value should be shared, while SwiftUI, AppKit, and WebKit adapters choose their platform-appropriate representation. ## Proposed architecture 1. Introduce a small, value-type semantic theme model (for example `ThemeID`, `ThemeVariant`, `ThemeTokens`, and a resolver) beside `AppearanceSettings`. A token must describe a role, not a component-specific arbitrary color. 2. Define a curated preset catalog with stable IDs, design rationale, and explicit light/dark variants. `AppearanceSettings` persists only a stable preset ID plus the existing appearance mode/variant choice and schema version; it does not persist an unbounded bag of RGB values. 3. Resolve `system` through the effective macOS color scheme. A user-selected light or dark mode must select the corresponding variant of the chosen preset. Unknown IDs, malformed persisted values, and future schema versions must fall back safely to the default preset without crashing or silently creating a new palette. 4. Make the resolved token set observable and inject it through the existing shell/settings environment. AppKit bridges (message-list rows, separators, window chrome) and the reader host should consume the same roles rather than independently deriving colors. 5. Keep the existing custom accent compatible during migration: either map it explicitly to the selection/accent roles or provide a one-time, documented fallback. Reset must remove stale custom theme state in a predictable way. Do not create a second persistence key that can disagree with `AccentSource`. 6. Model treatment policy separately from color tokens. A theme may express preferred dim/tint/material roles, but `WindowBackdropPlan` remains the enforcement point for Reduce Transparency, fullscreen, and opaque fallback. Native materials are optional implementation details, not a requirement of a preset. ### Semantic token families The initial role inventory should cover at least: - **Window and shell:** window canvas, elevated window surface, toolbar/titlebar surface, overlay/popover surface, dimming veil, and tint wash. - **Sidebar:** sidebar canvas, sidebar text hierarchy, selected row, hover row, disclosure/icon treatment, and unread badge foreground/background. - **Message list:** list canvas, row surface, hover row, selected row, unread emphasis, preview text, date/meta text, focus ring, and separators. - **Reader regions:** reader canvas plus distinct `readerSubjectTint`, `readerHeaderTint`, and `readerBodyTint` roles. Also provide reader subject/header/body text hierarchy and separators so the native reader chrome can make those regions visually distinct without blur, glass, or transparency. These are semantic roles only; final palette choices belong to the theme design and remain independent of the reader layout redesign. - **Text and content:** primary, secondary, tertiary, disabled, inverted, link, visited-link (if needed), and placeholder text. Ensure hierarchy is not conveyed by color alone. - **Interaction:** accent, selection fill, selected-content text, focus ring, keyboard-focus fill, hover, pressed, and disabled control states. Selection and focus must remain distinguishable in increased contrast. - **Structure and status:** separator, strong separator, border, warning, error, success, quarantine, find-highlight, and attachment/status treatments. Status roles must not rely on hue alone. - **Material policy:** preferred blur/glass/opaque treatment, maximum dimming, tint strength, and a high-contrast/Reduce Transparency fallback policy. These policy values must be bounded and resolved, not arbitrary user-entered effects. The resolver should be able to return a contrast-adjusted set for macOS Increased Contrast and a fully opaque set when Reduce Transparency is enabled. It must never use dimming or translucency to make primary text fail contrast. ## Presets Ship several named families with an intentional visual thesis and hand-authored light/dark pairs. For example: - **Paper & Ink:** warm paper surfaces, ink-like text hierarchy, restrained indigo interaction color, and quiet separators; the dark pair becomes charcoal/ink rather than merely inverting every value. - **Midnight Signal:** deep blue-black shell, cool slate list/reader surfaces, and a focused electric-blue interaction color; the light pair is a cool editorial white with the same information hierarchy. - **Graphite Workshop:** neutral graphite chrome, high-legibility near-white text, and a limited amber signal for interaction/status; the light pair uses mineral gray surfaces and keeps amber reserved for attention. These names and exact palettes are design decisions to validate, not permission to generate arbitrary combinations. Each preset needs written rationale, tested contrast, and a small preview that demonstrates the shell, sidebar, list, and reader-region roles. Do not add a general-purpose color editor in this work. ## Appearance settings behavior - Add a preset picker to the existing Appearance section with a compact visual preview and the light/dark/system behavior explained in text. - Selecting a preset updates the currently visible main and settings windows live, including AppKit-backed rows and reader chrome. Do not require a restart or window reopen. - Provide an explicit **Reset** action that restores the product default preset and clears the theme override (with existing system/light/dark mode semantics preserved unless the UI clearly says otherwise). Reset must be observable and testable. - Keep existing opacity and treatment controls coherent with the new policy. If a preset supplies a preferred treatment, explain whether the user override wins; do not leave two controls that claim ownership of the same state. - Persist and migrate deterministically. Cover old `AppearanceSettings` keys (`mode`, backdrop/treatment, opacity, message-list lines, and accent) and missing/invalid values. Theme IDs are versioned so a removed preset maps to a documented fallback rather than changing appearance unpredictably. ## Reader and HTML isolation Theme tokens may style Mailternal-owned reader chrome (subject, sender/recipient headers, full-header disclosure, separators, and the subject/header/body region surfaces) and the host fallback canvas. They must not rewrite the author-controlled email design. Scope any WebKit CSS variables/classes to the reader host, keep sanitized HTML in its content boundary, and preserve the current network-blocking and remote-image policy. A malicious message must not be able to inject a theme token, enable a material, load a remote stylesheet, or alter native chrome. Author colors, tables, links, and inline styles must remain isolated from shell/sidebar/list tokens; test both intentionally light and intentionally dark email fixtures. ## iOS direction Define the semantic role names and information hierarchy as the shared contract, not macOS view types, dimensions, or material names. A future iOS renderer should map window/sidebar/list/reader roles to phone-appropriate navigation, sheets, and full-width reader regions. It should preserve the same subject/header/body information principles while using a phone-specific layout and platform-native accessibility behavior. This issue does not implement iOS or require macOS and iOS screenshots to match pixel-for-pixel. ## Non-goals - No reader production-layout change, top-fade/padding change, header expansion implementation, or final reader palette decision. - No arbitrary user-authored palette editor, remote/downloaded themes, per-message themes, or theme scripting. - No change to HTML sanitization, remote-image permissions, link handling, or content security policy beyond the isolation hooks needed to consume owned reader roles. - No requirement to replace every native system semantic color at once; migrate the listed shell/list/reader roles deliberately and remove duplicate theme paths as each caller moves. - No iOS implementation in this ticket. ## QA and visual-regression plan - Unit-test token resolution for every preset and light/dark/system variant, malformed/unknown persisted IDs, migration from every existing appearance key, reset, Increased Contrast, and Reduce Transparency. Assert bounded opacity/tint values and contrast invariants for primary text, links, controls, selection, and focus. - Exercise live preview with both the main window and settings window open; verify SwiftUI and AppKit consumers update without reopening, and reset returns to the documented default. - Capture deterministic screenshots for each preset × light/dark variant, then repeat representative shell/list/reader states with Increased Contrast, Reduce Transparency, fullscreen, and narrow/large windows. Review separators, focus, selected/unread rows, status states, and reader region boundaries. - Run HTML isolation fixtures containing hostile inline colors, style tags, CSS variables, data images, blocked remote URLs, tables, and long untrusted text. Verify author styling remains content-local, native chrome stays themed, and no new network request is permitted. - Add accessibility checks for keyboard focus/selection, VoiceOver labels for preset and reset controls, color-independent status cues, Dynamic Type where applicable, and Reduce Motion/Reduce Transparency interactions. Record golden screenshot baselines and update them only with a reviewed design decision. ## Acceptance criteria - A curated catalog with at least three named, documented preset families exists, and every family has tested light and dark variants plus a system-resolution path. - One observable semantic token model drives window/sidebar/list/reader-owned surfaces, text hierarchy, interaction/selection, separators, status, and bounded dim/tint/material policy; reader subject/header/body tint roles are present. - Appearance settings provide a live preset preview and a working reset; changes propagate to SwiftUI and AppKit consumers without restart/reopen. - Persistence uses stable versioned IDs, migrates existing appearance/accent state, and safely falls back on invalid or removed values. - Reduce Transparency and Increased Contrast produce accessible resolved tokens and preserve an opaque fallback; no primary text or focus state depends on translucent material. - Sanitized HTML remains isolated: author colors do not leak into Mailternal chrome, Mailternal theme tokens do not rewrite author content, and the existing network-blocking boundary remains intact. - QA evidence covers unit resolution/migration, live preview/reset, accessibility states, visual-regression matrix, and hostile HTML fixtures. - Shared semantic roles and information principles are documented so future iOS mapping can be platform-specific without duplicating the macOS palette architecture. ## References - Current seams: `App/Sources/Support/AppearanceSettings.swift`, `App/Sources/Shell/WindowBackdrop.swift`, `App/Sources/Shell/MainWindowController.swift`, `App/Sources/Account/SettingsWindow.swift`, `App/Sources/MessageWeb/MessageWebView.swift`, and `App/Sources/Viewer/MessageViewer.swift`. - Apple Human Interface Guidelines — Color: https://developer.apple.com/design/human-interface-guidelines/color - Apple accessibility documentation: https://developer.apple.com/accessibility/ - Apple AppKit `NSVisualEffectView`: https://developer.apple.com/documentation/appkit/nsvisualeffectview - Apple WebKit `WKContentRuleList`: https://developer.apple.com/documentation/webkit/wkcontentrulelist - OWASP Cross Site Scripting Prevention Cheat Sheet: https://owasp.org/www-community/xss-prevention
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/mailternal#3
No description provided.