diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..8894c7d --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1 @@ +{"buildPath":"code"} diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..a5d23e2 --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,380 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-08-27T18:00:17.873Z", + "title": "Design System: Nostr Keynctr", + "extensions": { + "colorMeta": { + "warm-ivory": { + "role": "neutral", + "displayName": "Warm Ivory", + "canonical": "#f6f4f0", + "tonalRamp": [ + "#1e1b15", + "#3d3830", + "#5c564e", + "#7c756d", + "#9c958d", + "#bdb9b3", + "#deddda", + "#f6f4f0" + ] + }, + "paper": { + "role": "neutral", + "displayName": "Paper", + "canonical": "#ffffff", + "tonalRamp": [ + "#1f1b15", + "#3e3a34", + "#5d5953", + "#7d7872", + "#9c9791", + "#bcb6b0", + "#deddda", + "#ffffff" + ] + }, + "soft-stone": { + "role": "neutral", + "displayName": "Soft Stone", + "canonical": "#f1ede7", + "tonalRamp": [ + "#1d1b18", + "#3c3936", + "#5b5854", + "#7a7772", + "#999690", + "#b8b5ae", + "#d7d4cc", + "#f1ede7" + ] + }, + "ink": { + "role": "neutral", + "displayName": "Graphite Ink", + "canonical": "#201b15", + "tonalRamp": [ + "#201b15", + "#4d4741", + "#7a756d", + "#a7a39e", + "#d4d1ce", + "#eceae8", + "#f5f4f3", + "#faf9f8" + ] + }, + "ink-muted": { + "role": "neutral", + "displayName": "Ink Muted", + "canonical": "#6d655a", + "tonalRamp": [ + "#2a2722", + "#403d38", + "#56544f", + "#6d655a", + "#8a857e", + "#a7a5a1", + "#c4c2be", + "#e1dfdc" + ] + }, + "line": { + "role": "neutral", + "displayName": "Line", + "canonical": "#e6e0d6", + "tonalRamp": [ + "#2c2a27", + "#4a4845", + "#686663", + "#868481", + "#a4a29f", + "#c2c0bd", + "#e0dedb", + "#e6e0d6" + ] + }, + "antique-brass": { + "role": "primary", + "displayName": "Antique Brass", + "canonical": "#6d5ae6", + "tonalRamp": [ + "#221b4a", + "#3a2d7a", + "#523faa", + "#6d5ae6", + "#8b7cff", + "#a99dff", + "#c7bdff", + "#efecfd" + ] + }, + "antique-brass-soft": { + "role": "primary", + "displayName": "Brass Wash", + "canonical": "#efecfd", + "tonalRamp": [ + "#221b4a", + "#3a2d7a", + "#523faa", + "#6d5ae6", + "#8b7cff", + "#c7bdff", + "#efecfd", + "#f8f7ff" + ] + }, + "signal-coral": { + "role": "neutral", + "displayName": "Signal Coral", + "canonical": "#c0392b", + "tonalRamp": [ + "#3a1512", + "#5a1f1b", + "#7a2a24", + "#9a352d", + "#c0392b", + "#d66a60", + "#e99a96", + "#fdecec" + ] + }, + "muted-sage": { + "role": "neutral", + "displayName": "Muted Sage", + "canonical": "#22713c", + "tonalRamp": [ + "#0f2a18", + "#143d22", + "#1a502c", + "#22713c", + "#4a8a5f", + "#73a382", + "#9cbca5", + "#e7f5ec" + ] + }, + "antique-gold": { + "role": "neutral", + "displayName": "Antique Gold", + "canonical": "#9a6b13", + "tonalRamp": [ + "#2e2110", + "#4a3418", + "#664720", + "#825a28", + "#9a6b13", + "#b88a3a", + "#d6aa62", + "#fcf3e0" + ] + }, + "clear-ink": { + "role": "neutral", + "displayName": "Clear Ink", + "canonical": "#2868a4", + "tonalRamp": [ + "#0f2538", + "#163a58", + "#1d4f78", + "#2868a4", + "#4d82b4", + "#739dc4", + "#99b8d4", + "#e9f2fb" + ] + } + }, + "typographyMeta": { + "display": { + "displayName": "Display", + "purpose": "Page titles (h1) only — 24px, weight 650." + }, + "headline": { + "displayName": "Headline", + "purpose": "Card and modal heads (h2) — 16px, weight 650." + }, + "body": { + "displayName": "Body", + "purpose": "Reading text — 15px / 1.5, max 65–75ch." + }, + "label": { + "displayName": "Label", + "purpose": "Field labels and hints — 13px / 600." + }, + "mono": { + "displayName": "Mono", + "purpose": "Code and npub — SFMono 13.8px." + } + }, + "shadows": [ + { + "name": "paper-lift", + "value": "0 1px 2px rgba(30,25,18,0.05), 0 8px 24px rgba(30,25,18,0.06)", + "purpose": "Default card lift on Warm Ivory." + }, + { + "name": "modal-lift", + "value": "0 12px 40px rgba(20,15,10,0.22)", + "purpose": "Modal over app." + }, + { + "name": "neon-glow", + "value": "0 0 32px rgba(255,20,147,0.12)", + "purpose": "Gated neon modal glow only on dark." + } + ], + "motion": [ + { + "name": "ease-standard", + "value": "0.12s ease", + "purpose": "Button, nav, border-color transitions." + }, + { + "name": "ease-glass", + "value": "blur 18px saturate(160%)", + "purpose": "Aurora glass panels." + } + ], + "breakpoints": [ + { + "name": "sidebar", + "value": "920px" + }, + { + "name": "card-grid", + "value": "280px" + } + ] + }, + "components": [ + { + "name": "Primary Button", + "kind": "button", + "refersTo": "button-primary", + "description": "Main action — Antique Brass on Paper.", + "html": "", + "css": ".ds-btn-primary{background:var(--primary);color:var(--on-primary);border:1px solid transparent;border-radius:9px;padding:10px 18px;font-weight:600;transition:background 0.12s ease} .ds-btn-primary:hover{background:var(--primary-hover)} .ds-btn-primary:focus-visible{outline:2px solid var(--focus);outline-offset:2px}" + }, + { + "name": "Secondary Button", + "kind": "button", + "refersTo": "button-secondary", + "description": "Paper button with strong line border.", + "html": "", + "css": ".ds-btn-secondary{background:var(--surface);color:var(--text);border:1px solid var(--border-strong);border-radius:9px;padding:10px 18px;font-weight:600} .ds-btn-secondary:hover{background:var(--surface-hover)}" + }, + { + "name": "Ghost Button", + "kind": "button", + "refersTo": "button-ghost", + "description": "Transparent for tertiary actions.", + "html": "", + "css": ".ds-btn-ghost{background:transparent;color:var(--text-muted);border:1px solid transparent;border-radius:9px;padding:7px 12px;font-weight:600} .ds-btn-ghost:hover{background:var(--surface-2);color:var(--text)}" + }, + { + "name": "Danger Button", + "kind": "button", + "refersTo": "button-danger", + "description": "Signal Coral wash for destructive actions.", + "html": "", + "css": ".ds-btn-danger{background:var(--danger-soft);color:var(--danger);border:1px solid transparent;border-radius:9px;padding:7px 12px;font-weight:600} .ds-btn-danger:hover{background:var(--danger);color:#fff}" + }, + { + "name": "Card", + "kind": "card", + "refersTo": "card", + "description": "Paper surface with soft lift.", + "html": "
Active profile
Profile content
", + "css": ".ds-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;box-shadow:0 1px 2px rgba(30,25,18,0.05),0 8px 24px rgba(30,25,18,0.06);overflow:hidden} .ds-card-head{padding:16px 20px;border-bottom:1px solid var(--border);font-weight:650} .ds-card-body{padding:20px}" + }, + { + "name": "Input", + "kind": "input", + "refersTo": "input", + "description": "Paper field with strong line border.", + "html": "", + "css": ".ds-input{font:inherit;color:var(--text);background:var(--surface);border:1px solid var(--border-strong);border-radius:9px;padding:10px 12px;width:100%} .ds-input:focus{outline:none;border-color:var(--focus);box-shadow:0 0 0 3px color-mix(in srgb,var(--focus) 22%, transparent)}" + }, + { + "name": "Navigation Item", + "kind": "nav", + "refersTo": "nav-item", + "description": "Sidebar nav with muted → brass active.", + "html": "", + "css": ".ds-nav{display:flex;gap:12px;padding:11px 12px;border-radius:9px;border:none;background:transparent;color:var(--text-muted);font-weight:550} .ds-nav:hover{background:var(--surface-2);color:var(--text)} .ds-nav.is-active{background:var(--primary-soft);color:var(--primary)}" + }, + { + "name": "Undo Bar", + "kind": "custom", + "refersTo": "undo-bar", + "description": "Themed dismissal bar after delete — 5s, primary-soft.", + "html": "
Profile “Alice” deleted.—
", + "css": ".ds-undo{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:12px 0;padding:10px 14px;background:var(--primary-soft);border:1px solid var(--border);border-radius:8px} .ds-undo-link{background:none;border:none;padding:0;color:var(--primary);font-weight:600;cursor:pointer;text-decoration:underline;text-underline-offset:2px}" + }, + { + "name": "Badge", + "kind": "chip", + "refersTo": "badge", + "description": "Pill tag for state.", + "html": "Active", + "css": ".ds-badge{display:inline-flex;gap:5px;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:650;white-space:nowrap} .ds-badge.success{background:var(--success-soft);color:var(--success)}" + }, + { + "name": "Profile Edit Modal", + "kind": "custom", + "refersTo": "modal-profile-edit", + "description": "Paper modal for Rename / Picture / NIP-05 — Paper Lift, vault separation.", + "html": "

Edit profile — Alice

A

Public npub is visible to relays; private key never leaves Rust.

", + "css": ".ds-backdrop{position:fixed;inset:0;background:rgba(20,16,12,0.45);display:grid;place-items:center;padding:24px} .ds-modal{background:var(--surface);border:1px solid var(--border);border-radius:16px;box-shadow:0 12px 40px rgba(20,15,10,0.22);width:100%;max-width:520px;max-height:88vh;overflow:auto} .ds-modal-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:18px 22px;border-bottom:1px solid var(--border);font-weight:650} .ds-modal-body{padding:20px 22px;display:flex;flex-direction:column;gap:16px} .ds-avatar-lg{width:52px;height:52px;border-radius:50%;display:grid;place-items:center;background:var(--primary-soft);color:var(--primary);font-weight:700;font-size:21px} .ds-muted{color:var(--text-muted);font-size:13px;margin:0} .ds-field{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:600;color:var(--text)} .ds-modal-actions{display:flex;justify-content:flex-end;gap:10px;align-items:center;margin-top:4px} .ds-icon{width:34px;height:34px;border-radius:999px;border:1px solid var(--border);background:var(--surface)} [data-theme=\"glass\"] .ds-modal{backdrop-filter:blur(18px) saturate(160%);-webkit-backdrop-filter:blur(18px) saturate(160%)} [data-theme=\"neon\"] .ds-modal{box-shadow:0 12px 40px rgba(0,0,0,0.85),0 0 32px rgba(255,20,147,0.12)}" + } + ], + "narrative": { + "northStar": "Vault & Atelier", + "overview": "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.", + "keyCharacteristics": [ + "Warm and human, precise and calm", + "Paper and stone material language — tonal layering over heavy shadows", + "Semantic tokens only — every color has a job", + "Operate density — tasks complete faster because hierarchy is consistent", + "Vault logic made visible — locked/muted states are distinct at a glance" + ], + "rules": [ + { + "name": "The One Accent Rule", + "body": "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.", + "section": "colors" + }, + { + "name": "The Vault Separation Rule", + "body": "Secret-dependent UI never shares color with public data: private operations are flagged via muted-sage/signal-coral and explicit Approve/Reject.", + "section": "colors" + }, + { + "name": "The Weight Restraint Rule", + "body": "Only headings are 650. Do not add semibold to body or emphasize muted text with weight.", + "section": "typography" + }, + { + "name": "The Flat-By-Default Rule", + "body": "Surfaces are flat at rest. Shadows appear only as a response to layer (card on canvas, modal over app).", + "section": "elevation" + } + ], + "dos": [ + "Do keep Warm Ivory as the only page canvas on light", + "Do use Muted Sage wash only for successful security confirmation", + "Do keep one primary per screen — Antique Brass is rare by design", + "Do use var(--focus) 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" + ], + "donts": [ + "Don't use Signal Coral outside error/destructive states", + "Don't introduce a new accent hue outside the four semantic washes", + "Don't bump heading weight beyond 650 or add bold to muted text", + "Don't apply neon glow 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" + ] + } +} \ No newline at end of file diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index da13251..c73e18e 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,27 +1,29 @@ -# Checkpoint — fix profile delete/undo + themed auto-dismiss bar (2026-08-27) +# Checkpoint — Impeccable themes + ProfileEditModal (2026-08-27) ## Where things are - Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `9e635e7` ("fix: restore profile delete/undo and themed auto-dismiss undo bar"). Before it: `56c2a6d` (checkpoint: click profile row), `8130c3e` (click profile row on Home), `a5838ea`, `98f4520` etc. -- Working tree: clean except `KeynectrAppIconPossibility02.jpeg` untracked (long-standing, not part of commits). +- Git repo: `master` @ `8366af7` ("feat: add Impeccable themes (light + dark) + unified ProfileEditModal"). Before it: `832e114` (checkpoint: fix delete/undo), `9e635e7` (fix delete/undo bar), `56c2a6d` (click profile row), `8130c3e` etc. +- Working tree: clean except `.opencode/` (impeccable skill install, not tracked) and `KeynectrAppIconPossibility02.jpeg` untracked (long-standing). ## What was completed -1. **Fixed broken profile delete/undo.** `src/ipc.rs` was missing `DeleteProfile`/`UndoDelete` request variants and handlers, so `delete_profile` from renderer returned `Unexpected signer request`. Added `DeleteProfile {npub}`/`UndoDelete` to `Request`, handlers that call `profiles::delete_profile`/`app.undo_delete()`, save vault, push/pop `undo_history`, return `state_view()`. Exposed `profiles::delete_profile` outside `#[cfg(test)]` (was only inside tests). Updated `src/profiles.rs` accordingly. CLI `delete_profile_direct` kept for `main.rs`. -2. **Wired frontend to backend state.** `frontend/src/lib/api.ts`: `deleteProfile`/`undoDelete` now `call` (was `ProfileSummary`). `frontend/src/state/AppProvider.tsx`: both now use `applyState(...)` and type `Promise`; added `clearLastDeleted` that locally slices `undo_history` for UI dismissal. -3. **Themed auto-dismiss undo bar.** `frontend/src/screens/ProfilesScreen.tsx`: replaced permanent white bar (`var(--token-item-bg, #f0f0f0)` + "Profile deleted. You can restore it until you delete another or quit.") with themed bar (`var(--primary-soft)` bg, `var(--border)` border) showing `Profile "X" deleted. — Undo and restore profile` where the link uses `var(--primary)` (adapts to light/dark/neon/glass). Bar appears in both empty and populated states, auto-dismisses after 5s via `useEffect` watching `lastDeleted` (previous version fired unconditionally on mount). Fake backend updated to simulate delete/undo for tests. +1. **Fixed broken profile delete/undo** (previous). `src/ipc.rs` missing `DeleteProfile`/`UndoDelete`; added handlers returning `state_view`; exposed `profiles::delete_profile` outside tests; `api.ts`/`AppProvider` now `call` via `applyState`; `fakeBackend` delete/undo. +2. **Themed auto-dismiss undo bar.** Replaced permanent white bar with `var(--primary-soft)` + `var(--primary)` link `Undo and restore profile`, 5s `useEffect` watching `lastDeleted`, shown in both empty/populated states. +3. **Impeccable themes (light + dark).** `src/settings.rs`: `Theme::Impeccable` + `ImpeccableDark` (serde `impeccable-dark`); `types.ts` union extended; `styles.css` added `:root[data-theme='impeccable']` (oklch 97% lacquer light, kinpaku gold `oklch(77% .13 82)`, Alumni Sans 300) and `impeccable-dark` (oklch 15% lacquer-deep, champagne text), editorial refinements (uppercase labels, 8/3px radii, nav left-border active, card offset bar); `SettingsScreen.tsx` adds both options with live `var(--*)` swatches. +4. **Unified ProfileEditModal.** `frontend/src/components/ProfileEditModal.tsx` — Paper Lift modal (`var(--surface)`/`var(--border)`/`16px`/`0 12px 40px`), 3 tabs (Name/Picture/NIP-05) sharing `renameProfile`/`setProfilePicture`/`setNip05`; `ProfilesScreen.tsx` wires `Edit profile` (secondary) alongside legacy ghosts; `DESIGN.md` + `PRODUCT.md` + `.impeccable/design.json` from `impeccable document` (Vault & Atelier, warm ivory/charcoal/coral, 9 primitives). ## Commits added in this session (newest first) +- `8366af7` feat: add Impeccable themes (light + dark) + unified ProfileEditModal +- `832e114` checkpoint: fix delete/undo + themed auto-dismiss bar - `9e635e7` fix: restore profile delete/undo and themed auto-dismiss undo bar ## Verification commands run (all green) -- Rust: `cargo fmt --check` clean; `cargo clippy --all-targets` 2 pre-existing warnings; `cargo test` 115 passed; `cargo build --release` success -- Frontend (`frontend/`): `npm run typecheck` clean; `npm run lint` clean; `npm run format:check` clean (after `npm run format`); `npm test` 15 files / 99 tests passed; `npm run build` success; `npm run electron:build` success -- The `--ozone-platform=wayland` Vulkan and `has no handler with id` messages seen on `electron:build` are harmless Electron/Wayland warnings, not profile errors. +- Rust (repo root): `cargo fmt --check` clean; `cargo clippy --all-targets` 2 pre-existing warnings; `cargo test` 115 passed; `cargo build --release` success +- Frontend (`frontend/`): `npm test` 15 files / 99 tests passed; `npm run typecheck` clean; `npm run lint` clean; `npm run format:check` clean (after `npm run format`); `npm run build` success; `npm run electron:build` success +- Wayland `--ozone-platform` / `has no handler` messages on `electron:build` are harmless. ## How to resume / reproduce -GUI: `cargo build --release && cd frontend && npm run build && npm run electron:build && npm start` (or `npm run dev`). Create 2 profiles, delete one via Profiles → Delete → confirm, see themed undo bar for 5s with "Undo and restore profile" in primary color, click it to restore, or wait for auto-dismiss. -CLI: `cargo run -- delete-profile ` then `cargo run -- undo-delete` (in-memory undo only within same `serve` session; CLI undo across processes is not persisted). +GUI (Impeccable): `cargo build --release && cd frontend && npm run build && npm run electron:build && npm start` (or dev: `npm run dev` in one terminal + `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` in second). Settings → Appearance → `Impeccable — Light` / `Impeccable — Dark` (gold on ivory lacquer vs lacquer-deep). Profiles → Delete → see themed undo bar 5s; Edit profile → tabbed modal. CLI: `cargo run -- settings set theme impeccable` / `impeccable-dark`. ## Outstanding / next-step items -- Undo restores profile with empty `secret_key` (`StoredProfile { secret_key: "" }`) — existing design stores `ProfileSummary` in `undo_history` so secret cannot be recovered. Needs full `StoredProfile` in undo stack if secret preservation is required. -- `KeynectrAppIconPossibility02.jpeg` remains untracked; `concept3.svg` deletion already committed earlier. +- Undo restores with empty `secret_key` (stores `ProfileSummary`); needs `StoredProfile` in `undo_history` for full secret recovery. +- `.opencode/` skill files and `KeynectrAppIconPossibility02.jpeg` remain untracked; not part of theme commits. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..b496f5c --- /dev/null +++ b/DESIGN.md @@ -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 `-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 ` + ))} + + + {tab === 'name' && ( +
+ + setLabel(e.target.value)} + placeholder="My Profile" + autoFocus + /> +
+ + +
+
+ )} + + {tab === 'picture' && ( +
+ + setPictureUrl(e.target.value)} + placeholder="https://…/avatar.png" + /> + + {nipProblem && {nipProblem}} +
+ {target.picture && ( + + )} + + +
+
+ )} + + {tab === 'nip05' && ( +
+ + setNip05Value(e.target.value)} + placeholder="name@domain.com" + /> + {nipProblem && nip05.trim() && {nipProblem}} +

+ Also serve /.well-known/nostr.json on your domain — see{' '} + docs/NIP-05.md. +

+
+ {target.nip05 && ( + + )} + + +
+
+ )} + + + ); +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 08455e1..0d44331 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -1,4 +1,4 @@ -export type Theme = 'light' | 'dark' | 'glass' | 'neon'; +export type Theme = 'light' | 'dark' | 'glass' | 'neon' | 'impeccable' | 'impeccable-dark'; /** Lifecycle of the NIP-46 remote signer. */ export type SignerPhase = 'stopped' | 'connecting' | 'connected'; diff --git a/frontend/src/screens/ProfilesScreen.tsx b/frontend/src/screens/ProfilesScreen.tsx index f74c483..fdd796b 100644 --- a/frontend/src/screens/ProfilesScreen.tsx +++ b/frontend/src/screens/ProfilesScreen.tsx @@ -7,6 +7,7 @@ import { EmptyState } from '../components/EmptyState'; import { ErrorText } from '../components/ErrorText'; import { Icon } from '../components/Icon'; import { Modal } from '../components/Modal'; +import { ProfileEditModal } from '../components/ProfileEditModal'; import { ShowSecretKeyModal } from '../components/ShowSecretKeyModal'; import { formatDate, shortenNpub } from '../lib/format'; import type { MetadataPublishReport } from '../lib/types'; @@ -34,6 +35,12 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) { const [pictureTarget, setPictureTarget] = useState(null); const [renameTarget, setRenameTarget] = useState<{ npub: string; label: string } | null>(null); const [nip05Target, setNip05Target] = useState(null); + const [editTarget, setEditTarget] = useState<{ + npub: string; + label: string; + picture?: string | null; + nip05?: string | null; + } | null>(null); const profiles = state?.profiles ?? []; const shorten = state?.settings.shorten_npub ?? true; @@ -244,6 +251,20 @@ export function ProfilesScreen({ onCreateProfile }: ProfilesScreenProps) { > Publish name +