feat: add Impeccable themes (light + dark) + unified ProfileEditModal

- settings: add Theme::Impeccable / ImpeccableDark (serde rename impeccable-dark)
- types: extend Theme union
- styles: add :root[data-theme=impeccable] (oklch lacquer/kinpaku) and impeccable-dark (lacquer-deep), Alumni Sans display, editorial refinements
- SettingsScreen: add Impeccable options with live swatch preview
- ProfilesScreen: wire ProfileEditModal (Paper Lift) with segmented tabs, keep legacy modals for compat
- DESIGN.md/PRODUCT.md + .impeccable/design.json from impeccable document
This commit is contained in:
Avi 2026-08-27 14:12:52 -05:00
commit 8366af7d86
10 changed files with 1258 additions and 3 deletions

263
DESIGN.md Normal file
View file

@ -0,0 +1,263 @@
---
name: Nostr Keynctr
description: Local-first Nostr identity atelier — keys stay in Rust, the UI stays calm and warm.
colors:
warm-ivory: "#f6f4f0"
soft-stone: "#f1ede7"
stone-hover: "#faf8f5"
paper: "#ffffff"
ink: "#201b15"
ink-muted: "#6d655a"
line: "#e6e0d6"
line-strong: "#d8d0c3"
antique-brass: "#6d5ae6"
antique-brass-hover: "#5c49d4"
antique-brass-soft: "#efecfd"
on-brass: "#ffffff"
signal-coral: "#c0392b"
signal-coral-soft: "#fdecec"
antique-gold: "#9a6b13"
antique-gold-soft: "#fcf3e0"
muted-sage: "#22713c"
muted-sage-soft: "#e7f5ec"
clear-ink: "#2868a4"
clear-ink-soft: "#e9f2fb"
focus: "#6d5ae6"
typography:
display:
fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif"
fontSize: "24px"
fontWeight: 650
lineHeight: 1.25
letterSpacing: "normal"
headline:
fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif"
fontSize: "16px"
fontWeight: 650
lineHeight: 1.25
letterSpacing: "normal"
body:
fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif"
fontSize: "15px"
fontWeight: 400
lineHeight: 1.5
letterSpacing: "normal"
label:
fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif"
fontSize: "13px"
fontWeight: 600
lineHeight: 1.5
letterSpacing: "normal"
mono:
fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace"
fontSize: "13.8px"
fontWeight: 400
lineHeight: 1.5
letterSpacing: "normal"
rounded:
sm: "9px"
md: "14px"
lg: "16px"
pill: "999px"
spacing:
xs: "8px"
sm: "12px"
md: "16px"
lg: "20px"
xl: "32px"
components:
button-primary:
backgroundColor: "{colors.antique-brass}"
textColor: "{colors.on-brass}"
rounded: "{rounded.sm}"
padding: "10px 18px"
button-primary-hover:
backgroundColor: "{colors.antique-brass-hover}"
textColor: "{colors.on-brass}"
rounded: "{rounded.sm}"
padding: "10px 18px"
button-secondary:
backgroundColor: "{colors.paper}"
textColor: "{colors.ink}"
rounded: "{rounded.sm}"
padding: "10px 18px"
button-ghost:
backgroundColor: "transparent"
textColor: "{colors.ink-muted}"
rounded: "{rounded.sm}"
padding: "7px 12px"
button-danger:
backgroundColor: "{colors.signal-coral-soft}"
textColor: "{colors.signal-coral}"
rounded: "{rounded.sm}"
padding: "7px 12px"
card:
backgroundColor: "{colors.paper}"
textColor: "{colors.ink}"
rounded: "{rounded.md}"
padding: "18px"
input:
backgroundColor: "{colors.paper}"
textColor: "{colors.ink}"
rounded: "{rounded.sm}"
padding: "10px 12px"
---
# Design System: Nostr Keynctr
## Overview
**Creative North Star: "Vault & Atelier"**
Nostr Keynctr is an atelier, not a dashboard — a warm, quiet workshop where identity work is done with care. The space feels like heavy paper and soft stone, with ink that is near-black, not pure black. Instruments are laid out plainly; nothing shouts for attention. Trust is built through precision: consistent edges, settled type, and state that is always legible. The product truth — keys never leave Rust — is mirrored visually: the UI is restrained, the material is honest, and every destructive or security-relevant moment is given deliberate weight.
The aesthetic is *warm and human*, not technical or bold. Density is Operate: scannable lists, clear hierarchies, and generous but not loose spacing (8/12/16/20/32). The four themes (light, dark, glass/Aurora, neon) share the same semantic roles; only the material values shift. Neon and glass are gated expressions, never the default.
**Key Characteristics:**
- Warm and human, precise and calm — humanity first, then exactness.
- Paper and stone material language — tonal layering over heavy shadows.
- Semantic tokens only — every color has a job (public data vs. private operation vs. relay health vs. security).
- Operate density — tasks complete faster because hierarchy is consistent.
- Vault logic made visible — locked/muted states are distinct at a glance.
## Colors
Four themes share the same semantic roles; values below are the light (paper) baseline extracted from `frontend/src/styles.css` (`:root` / `:root[data-theme='light']`). Dark, neon, and glass override the same tokens (see Elevation notes for glass treatment).
### Primary
- **Antique Brass — restrained identity accent** (`{colors.antique-brass}` #6d5ae6): Used for primary actions, active nav (`is-active`), focus rings, and the themed undo bar link ("Undo and restore profile"). Restrained — ≤10% of any screen. Hover is `{colors.antique-brass-hover}` #5c49d4; wash is `{colors.antique-brass-soft}` #efecfd.
- **Focus Violet** (`{colors.focus}` #6d5ae6): Same value as primary; outlines via `outline: 2px solid var(--focus)`.
### Neutral
- **Warm Ivory — canvas** (`{colors.warm-ivory}` #f6f4f0): `var(--bg)` — page background.
- **Paper — surface** (`{colors.paper}` #ffffff): `var(--surface)` — cards, sidebar, modals.
- **Soft Stone — raised/tinted surface** (`{colors.soft-stone}` #f1ede7): `var(--surface-2)` — inline code, secondary surfaces, segmented control track, `create-explainer`.
- **Stone Hover — hover wash** (`{colors.stone-hover}` #faf8f5): `var(--surface-hover)`.
- **Ink — primary text** (`{colors.ink}` #201b15): `var(--text)` — near-black, never pure black.
- **Ink Muted — secondary text** (`{colors.ink-muted}` #6d655a): `var(--text-muted)` — subtitles, hints, `.muted`, dates.
- **Line — hairline border** (`{colors.line}` #e6e0d6): `var(--border)`.
- **Line Strong — emphasized border** (`{colors.line-strong}` #d8d0c3): `var(--border-strong)` — input strokes, secondary buttons.
### Semantic Feedback
- **Muted Sage — secure/success** (`{colors.muted-sage}` #22713c / wash #e7f5ec): active profile border, success badges/alerts. Not decorative — only for successful security-state confirmation.
- **Signal Coral — destructive/warning/error** (`{colors.signal-coral}` #c0392b / wash #fdecec): danger buttons, error alerts, relay bad dots. Destructive actions live here alone.
- **Antique Gold — caution** (`{colors.antique-gold}` #9a6b13 / wash #fcf3e0): warning badges/alerts.
- **Clear Ink — information/relay** (`{colors.clear-ink}` #2868a4 / wash #e9f2fb): info badges/alerts, relay neutral states.
- **On Brass — text on accent** (`{colors.on-brass}` #ffffff): label inside primary buttons.
**Theme overrides (same roles, different material):** Dark inverts to ink surfaces (`#211d29`, `#2a2534`) with primary `#8b7cff`; Neon uses pure black panels (`#000000`) and Cyber Pink primary `#ff1493` + Aurora Purple/Yellow highlights; Glass (Aurora) uses translucent `rgba(23,29,41,0.58)` surfaces with `backdrop-filter: blur(18px) saturate(160%)` and cyan primary `#7fdbff`. Tokens stay semantically identical, so components do not branch per theme.
### Named Rules
**The One Accent Rule.** Antique Brass is the only saturated accent in the restrained palette. Its rarity is the point — use it for primary actions and active state only.
**The Vault Separation Rule.** Secret-dependent UI never shares color with public data: private operations are flagged via `muted-sage`/`signal-coral` and explicit Approve/Reject, never via primary alone.
## Typography
**Display Font:** System UI stack (`system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif`) — no custom display face; trust is in weight and rhythm, not ornament.
**Body Font:** Same System UI stack — continuity over contrast.
**Label/Mono Font:** `SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace` — code and `npub` only.
**Character:** Humanist, quiet, and precise. Weight 650 for headings is the only emphasis; body is regular 15/1.5 for long-form readability without dense compaction.
### Hierarchy
- **Display** (650, 24px, 1.25): `h1` page titles only — one per screen.
- **Headline** (650, 16px, 1.25): `h2` card heads, modal titles.
- **Title** (650, 16px, 1.25): Profile/cluster names (`.profile-name`, `.profile-card-meta h3`).
- **Body** (400, 15px, 1.5): All reading text, `p`, `li`, `.page-subtitle` (14px muted variant). Max width ~65–75ch in cards.
- **Label** (600, 13px): Field labels, `.hint`, `.char-count`, `.field > label`.
- **Mono** (400, 13.8px / 0.92em): `code`, `.mono`, npub displays — always on `Soft Stone` pill.
### Named Rules
**The Weight Restraint Rule.** Only headings are 650. Do not add semibold to body or emphasize muted text with weight — color shift (`ink` → `ink-muted`) is the emphasis.
## Layout
**Canvas:** Centered column (`max-width: 920px`, `margin: 0 auto`) with screen padding `32px 40px 56px` (collapses proportionally on narrow viewports). Inner stack via `.screen-inner` (`gap: 20px`, `flex-direction: column`).
**Shell:** Fixed sidebar `248px` + flexible `.main` (`overflow-y: auto`). Sidebar is paper on light, translucent blurred pane on glass. App is `height: 100vh` locked; only main scrolls.
**Grids:** Home is single-column (`gap: 20px`). Profiles use responsive `repeat(auto-fill, minmax(280px, 1fr))` (`gap: 16px`). Relays are vertical list (`gap: 12px`). Compose card keeps tabs above editor; preview stacks below.
**Spacing rhythm:** 8/12/16/20/32. Section gaps 20px; card internal padding 18–20px; button padding `10px 18px` (md) / `7px 12px` (sm); input padding `10px 12px`. No arbitrary values — step on the scale.
**Responsive:** Flex-wrap at page heads and action rows; segmented controls wrap; profile grid collapses to single column; modal max-width `520px`, max-height `88vh`.
## Elevation & Depth
System is **flat-by-default, tonal before lifted.** Depth is conveyed first by material contrast (Warm Ivory → Paper → Soft Stone) and 1px `Line` borders. Shadows are whisper-low and never the primary separation.
### Shadow Vocabulary
- **Paper Lift** (`box-shadow: 0 1px 2px rgba(30,25,18,0.05), 0 8px 24px rgba(30,25,18,0.06)`): Cards, profile cards — the default lift for paper surfaces on ivory.
- **Modal Lift** (`box-shadow: 0 12px 40px rgba(20,15,10,0.22)`): Modals — only overlay that earns a large, soft shadow. On dark: `0 12px 40px rgba(0,0,0,0.6)`.
- **Neon Glow (gated)** (`0 0 32px rgba(255,20,147,0.12)` paired with dark shadows): Only in `data-theme='neon'` modals — never on light/paper.
**Glass/Aurora exception:** Panels become translucent (`rgba(23,29,41,0.58)`, `rgba(255,255,255,0.05)`) with `backdrop-filter: blur(18px) saturate(160%)`; body carries three radial gradients (cyan/purple/aqua washes) to create depth through blur, not shadow.
### Named Rules
**The Flat-By-Default Rule.** Surfaces are flat at rest. Shadows appear only as a response to layer (card on canvas, modal over app). Never add shadow to inline controls or navigation.
## Shapes
Form language is *softly squared with a pill exception.* Corners are gentle, never sharp, never fully rounded except for tags and avatars.
- **Soft square** (`border-radius: var(--radius)` 14px): Cards, modals (16px), empty states — the workhorse.
- **Compact square** (`border-radius: var(--radius-sm)` 9px): Buttons, inputs, alerts, segmented control, nav items.
- **Pill** (`border-radius: 999px`): Badges, `npub-chip`, toggles, compose tabs, segmented buttons.
- **Circle** (`50%`): Avatars (42px / 52px lg), toggles thumb.
- **Empty-state dash** (`border: 1.5px dashed var(--border-strong)`): Deliberately unfinished — signals "bring your own."
- **Glass blur** is shape-adjacent: panels keep the same radii but gain `backdrop-filter` to soften edges without changing geometry.
## Components
### Buttons
- **Shape:** Compact square (9px), `font-weight: 600`, `gap: 8px`.
- **Primary (Antique Brass on Paper):** `background: var(--primary)` / `color: var(--on-primary)` / `padding: 10px 18px` (md) or `7px 12px` (sm). Hover → `var(--primary-hover)`. Loading shows spinner in `rgba(255,255,255,0.4)` track.
- **Secondary:** `background: var(--surface)` / `border: 1px solid var(--border-strong)` / `color: var(--text)` / Hover → `var(--surface-hover)`.
- **Danger:** `background: var(--danger-soft)` / `color: var(--danger)` / Hover → `var(--danger)` on white.
- **Ghost:** `background: transparent` / `color: var(--text-muted)` / Hover → `var(--surface-2)` + `var(--text)`.
- Focus everywhere: `outline: 2px solid var(--focus)` `offset: 2px`.
### Chips / Badges
- **Style:** Pill (999px), `padding: 3px 10px`, `font-size: 12px` / `weight: 650`, neutral uses `var(--surface-2)` + `var(--border)`, semantic variants use `<role>-soft` washes.
### Cards / Containers
- **Corner Style:** Soft square (14px).
- **Background:** `var(--surface)` (or translucent on glass).
- **Shadow Strategy:** Paper Lift by default; see Elevation.
- **Border:** `1px solid var(--border)` (active profile card upgrades to `var(--success)`).
- **Internal Padding:** `18px` (profile card) / `20px` (card-body).
### Inputs / Fields
- **Style:** `background: var(--surface)` / `border: 1px solid var(--border-strong)` / `radius: 9px` / `padding: 10px 12px` / `font: inherit`.
- **Focus:** `border-color: var(--focus)` + `box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 22%, transparent)`.
- **Error:** `.field-error` in `var(--danger)` 13px / 500.
- **Hint:** `.hint` in `var(--text-muted)` 13px.
### Navigation (Sidebar)
- **Style:** Sidebar `248px`, `background: var(--surface)`, `border-right: 1px solid var(--border)`. Brand block with `38px` white tile (inverted `filter: invert(1)` on dark/neon/glass).
- **Nav item:** `padding: 11px 12px` / `radius: 9px` / `font-weight: 550` / `color: var(--text-muted)` → hover `var(--surface-2)` + `var(--text)` → active `var(--primary-soft)` + `var(--primary)`.
- **Footer:** Profile row + top border `1px solid var(--border)`.
### Undo Bar (Signature Component)
- **Shape:** Compact square (`8px`), `padding: 10px 14px`, `border: 1px solid var(--border)`, `background: var(--primary-soft)`.
- **Character:** Warm, not alarming — confirms deletion without shouting. Label in `var(--text)` 500, dash in `var(--text-muted)`, action as themed link.
- **Action:** Inline `<button>` styled as link (`background: none` / `color: var(--primary)` / `font-weight: 600` / `underline` `2px` offset). Auto-dismisses after 5s; restores via `undoDelete`. Same token treatment on all themes.
## Do's and Don'ts
### Do:
- **Do** keep Warm Ivory (`#f6f4f0`) as the only page canvas on light; let Paper and Soft Stone carry the hierarchy.
- **Do** use Muted Sage wash only for successful security confirmation (active badge, save success).
- **Do** keep one primary per screen — Antique Brass is rare by design; secondary actions stay ghost or secondary.
- **Do** use `var(--focus)` (same as primary) for every focus ring; keep the 2px + 2px offset invariant.
- **Do** preserve the side-by-side rate: cards on Paper with 1px Line borders before adding any shadow.
- **Do** let glass blur inherit radii; never change corner size just for translucency.
### Don't:
- **Don't** use Signal Coral outside error/destructive states — never for decorative accents or neutral badges.
- **Don't** introduce a new accent hue outside the four semantic washes (primary/success/warning/danger/info); add a new feature with existing tokens.
- **Don't** bump heading weight beyond 650 or add bold to muted text — shift color to Ink Muted instead.
- **Don't** apply neon glow (pink `0 0 32px rgba(255,20,147,0.12)`) outside `data-theme='neon'` modals.
- **Don't** invent a new radius — step on `9px` / `14px` / `16px` / `999px` only.
- **Don't** replace the 5s auto-dismiss undo bar with a permanent modal; the atelier stays calm after a task.