SPEC-0006: Web UI redesign (slate)
- Status: Accepted
- Date: 2026-06-28
- Capability: web-ui-redesign
- Source packages:
internal/web(templates, render, server),internal/store(small additions),internal/web/tailwind/input.css - Related ADRs: π ADR-0012 (slate design system), π ADR-0007 (Tailwind + daisyUI), π ADR-0006 (web stack/CSP), π ADR-0010 (privacy posture)
- Design source: docs/design/redesign-handoff.md
- Extends / supersedes (visually): SPEC-0004 (web-ui)
Overviewβ
This spec defines the "slate" redesign of the msgbrowse web UI: a bespoke dark
design system implemented as a custom daisyUI theme (+ a derived light variant),
a dense-log transcript replacing chat bubbles, and redesigned Home, Search,
Media, and Journal screens. All existing constraints hold: server-rendered
html/template + HTMX, no Node at runtime, strict CSP, system fonts, Heroicons.
The design handoff is the pixel-level source of truth; this spec captures the
testable requirements and the slate tokens.
Design tokens (normative)β
- Surfaces: base
#0f1216, raised panel#13171c, navbar/sidebar#161a20, hover/selected tint#1b2330/rgba(111,147,214,.05β.12). - Borders: primary
#232a33; subtle#1c2128/#1c222a/rgba(35,42,51,.55). - Text: primary
#dbe2ea(headings up to#eaf1fb), secondary.5β.7alpha, dim/meta.32β.42alpha. - Accent: slate blue
#6f93d6; light accent (the "Me" name)#9bb6e6; text on accent#0f1216. Search<mark>: bgrgba(232,184,75,.28), text#f0d28a. - Source colors: Signal
#3b82f6(pill text#6f93d6/ borderrgba(111,147,214,.35)), iMessage#34c759(pill text#7bd88f/ borderrgba(52,199,89,.3)). - Avatar palette (hashβindex):
#f43f5e #0ea5e9 #f59e0b #14b8a6 #d946ef #f97316 #6366f1 #10b981. - Type: system sans (UI); system mono for timestamps/filenames/counts;
tabular-numson all counts. Scale (px): meta 10.5β12 Β· body 13β14.5 Β· row titles 14 Β· H1 21β26 Β· home hero 38. Heading tracking-.01emβ¦-.025em. - Shape: radius rows/cards 8β14, inputs 8β11, avatars 50% (or 6px dense), pills 5β8, frame 14β16. In-app elevation via borders, not shadows.
- Layout: full-width unified header ~52px (amended by #190; originally a
54px navbar), sidebar 320px, transcript column max-width 640px
(680px editorial). Centered screen columns: Home ~780px, Journal ~880px. Use
flex/grid
gap, not margins.
Requirementsβ
REQ-0006-001: Slate theme foundationβ
The UI MUST render in the slate tokens via a custom daisyUI theme set as the
default (dark). A derived light variant MUST exist and the header toggle MUST
switch between them, persisted to localStorage and applied before paint (no
FOUC). Numerals in counts/timestamps/filenames MUST use a mono face and
tabular-nums. No web fonts or external assets may be introduced (CSP holds).
Scenario: Theme applied without FOUCβ
- Given a saved theme preference
- When any page loads
- Then the slate (or light) tokens apply before first paint and counts render in tabular mono.
REQ-0006-002: App shell β headerβ
Amended by #190 (shell redesign). Originally: a 54px navbar with the accent home glyph + msgbrowse wordmark, right-aligned global counts in dim mono, and a 34px circular settings button. The global counts moved to Status (Settings β Diagnostics) in #152; #190 promoted the toolbar to a full-width header owning the primary navigation.
The app MUST render one full-width unified header (~52px) as a direct child of
<body>, spanning the window above both the sidebar and the content column.
It MUST show: LEFT β the below-md sidebar toggle and a contextual title
(msgbrowse on home/global surfaces, the active conversation name on a
transcript) that links home; CENTER β a Messages/Media segmented tab pair (the
primary nav), kept in normal flow and centered within the header's available
space so it can never overlap the side clusters at any viewport width; RIGHT β
a search pill (an icon-only /search link below sm), the theme toggle, and a
settings gear (the sole Settings entry). The header MUST NOT show global
counts.
Scenario: Header tabs and contextual titleβ
- Given an open conversation
- When the page renders
- Then the header shows the conversation name on the left (linking home), the centered Messages/Media tabs with Messages active, and the search/theme/settings cluster on the right β with no global counts.
REQ-0006-003: App shell β sidebarβ
Amended by #190 (shell redesign). Originally the sidebar also carried nav links with leading icons (Search Β· Media Β· Journal Β· Status & backups); those surfaces moved to the header β the centered Messages/Media tabs and the search pill β leaving the sidebar list-only.
The 320px sidebar MUST be list-only: a filter input ("Filter conversations"),
a PINNED section (when any conversation is pinned), and a CONVERSATIONS header
with a right-aligned total β no nav links. Conversation rows MUST be two lines
(avatar+presence Β· name Β· right-aligned mono count / last-message preview with
Me: prefix when sent by the owner, truncated). The selected row MUST show the
#1b2330 tint and a left accent rail. Typing in the filter MUST narrow the
list. Below md the sidebar MUST present as an overlay drawer toggled from the
header; at md and above it MUST be pinned open.
Scenario: Selected conversation rowβ
- Given a conversation is open
- When the sidebar renders
- Then its row shows the accent left rail + selected tint, and the filter narrows the list as typed.
REQ-0006-004: Avatars, presence dots, source pillsβ
Avatars MUST be a colored circle (6px rounded square in dense lists) with white 600-weight initials, colored deterministically from the avatar palette by a hash of the name. A presence dot MUST sit bottom-right, colored by source (Signal blue / iMessage green) with a 2px border matching the surface behind it. Source MUST also be expressible as a colored pill (Signal/iMessage).
Scenario: Source-derived presenceβ
- Given a Signal conversation and an iMessage conversation
- When their rows/headers render
- Then each shows the correct presence-dot color and, where shown, the matching source pill.
REQ-0006-005: Conversation reader β dense-log transcriptβ
The transcript MUST use the dense-log layout (NOT chat bubbles): a left mono
timestamp gutter (~76px, HH:MM:SS, dim), a 3px sender-colored rail (accent for
"Me"), and a content column (max-width 640px) with the sender name (12.5px/600;
"Me" in #9bb6e6) above the body (14px, line-height ~1.55). It MUST render: a
faint accent wash + accent edge on "Me" rows; centered day separators; centered
italic/mono system events; attachment chips (paperclip + mono filename + size);
link-preview pills; image placeholders (real decoded media in production); and
quoted blocks. Consecutive messages from the same sender within a short window
MUST be grouped. Large histories MUST scroll without loading all rows at once
(keep the existing HTMX keyset paging; virtualization optional).
Scenario: Dense-log rowβ
- Given an open conversation
- When the transcript renders
- Then each message shows the timestamp gutter, sender-colored rail, name, and body; "Me" rows carry the accent wash; day separators and system events render as non-message rows; and no
chat-bubblemarkup remains.
REQ-0006-006: Conversation headerβ
The header MUST show the avatar + name (22β26px/700), inline pill chips for
phone/email identifiers, and a meta line (N messages Β· <date range>) with
icon+count chips for images / links / attachments.
Scenario: Identifier chipsβ
- Given a contact with a merged phone and email
- When the conversation header renders
- Then the phone and email render as inline pill chips alongside the message/range/media meta.
REQ-0006-007: Home landingβ
Home MUST render a centered column with: the hero wordmark + tagline; a 3-cell stat strip (Conversations / Messages / Newest message) with mono tabular values; an "On this day" card surfacing a resurfaced message from a prior year (when one exists); a "Jump back in" card listing recent conversations with relative times; and a quick-links grid (Search / Media / Journal / Status).
Scenario: On this dayβ
- Given messages exist from the same calendar day in a prior year
- When Home renders
- Then the "On this day" card shows one such message with a link to that day; absent any, the card is omitted or shows an empty state.
REQ-0006-008: Search redesignβ
Search MUST show a large field, a wrapping filter row (conversation, source,
from/to date, sender, Has-attachment / Has-link toggles, accent Search button),
a results meta line with count and elapsed time, and result cards (avatar Β·
conversation Β· sender Β· timestamp Β· source pill Β· snippet with <mark>). The
top/active result MAY carry an accent border. Live filtering MUST refine results
as the query/filters change.
Scenario: Results with timingβ
- Given a query that matches messages
- When results render
- Then the meta line shows
<count> results Β· <elapsed>and each card shows a source pill and a highlighted snippet.
REQ-0006-009: Media redesignβ
Media MUST show tabs (Images / Files / Links) with count badges and an accent underline on the active tab, a filter row, and for Images a multi-column grid of square cover tiles with a bottom scrim + mono filename; clicking a tile MUST open a lightbox navigable within the current tab/filter set.
Scenario: Image grid + lightboxβ
- Given images in the archive
- When the Images tab renders
- Then square tiles show filenames over a scrim and clicking one opens a lightbox.
REQ-0006-010: Pinned conversationsβ
A conversation MUST be pinnable; pinned conversations MUST appear in the sidebar PINNED section. Pin state MUST persist (schema) and a pin/unpin affordance MUST exist.
Scenario: Pin persistsβ
- Given a pinned conversation
- When the app reloads
- Then it still appears under PINNED.
REQ-0006-011: Status & backups re-skinβ
The Status & backups screen MUST adopt the slate system (surfaces, borders, mono tabular values, source-grouped runs) with no behavioral change.
Scenario: Status in slateβ
- Given ingest runs and snapshots
- When Status renders
- Then it uses slate surfaces/borders and mono tabular values.
REQ-0006-012a: Journal screen shell (testable now)β
The Journal screen MUST render the slate layout β daily editorial card (accent eyebrow, prose region, People/Themes/Mood meta row), month strip, and per-day cards β from a stubbed/fixture day summary, independent of the LLM backend. This is verifiable at merge with a fixture and MUST NOT depend on live digest generation.
Scenario: Editorial card renders from a fixtureβ
- Given a fixture day summary (prose + people/themes/mood)
- When the Journal screen renders
- Then it shows the eyebrow, prose, and People/Themes/Mood meta in the slate layout.
REQ-0006-012b: Journal wired to real digests β BLOCKED on backendβ
Once the journal digest backend exists, the Journal screen MUST render real generated summaries (per-day, cached) over the raw messages. This requirement is BLOCKED on the journal digest backend (roadmap Slice 6) and is deferred β it is NOT counted against this epic's mergeable slices.
Scenario: Real editorial dayβ
- Given a generated editorial summary for a day
- When the Journal screen renders that day
- Then it shows the generated prose and People/Themes/Mood derived from that day's threads.