diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..56178ef --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,41 @@ +{ + "root": true, + "env": { + "browser": true, + "webextensions": true, + "serviceworker": true, + "es2022": true + }, + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "no-eval": "error", + "no-implied-eval": "error", + "no-undef": "error", + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] + }, + "ignorePatterns": [ + "node_modules/", + "dist/", + "**/*.bundle.js" + ], + "overrides": [ + { + "files": ["test/**/*.js", "scripts/**/*.js"], + "env": { + "node": true, + "browser": false + } + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..19d4dd9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + build-test-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Test + run: npm test + + - name: Lint + run: npm run lint + + - name: Package extension (sideloadable unpacked zip) + run: | + mkdir -p dist + zip -r dist/podkey-extension.zip manifest.json src popup $([ -d icons ] && echo icons) -x '*.test.js' + + - name: Upload extension artifact + uses: actions/upload-artifact@v4 + with: + name: podkey-extension + path: dist/podkey-extension.zip + if-no-files-found: error diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f273f7..42d26f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to Podkey will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **NIP-44 (v2) encryption** — `window.nostr.nip44.encrypt(pubkey, plaintext)` + and `window.nostr.nip44.decrypt(pubkey, ciphertext)`, enabling NIP-17 / NIP-59 + gift-wrapped direct messages. Crypto runs in the background service worker + (the private key never reaches the page), reusing the existing message-passing + path. Implemented with `@noble/ciphers` (chacha20) + `@noble/hashes` + (hkdf/hmac/sha256) + `@noble/secp256k1` (ECDH). Verified against the official + NIP-44 spec test vectors. + ## [0.0.7] - 2024-12-XX ### Changed diff --git a/README.md b/README.md index b12cc78..eb19d56 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ - ✅ **Key Generation** - Secure cryptographic key generation using `@noble/secp256k1` - ✅ **Key Import** - Import existing 64-char hex private keys - ✅ **Event Signing** - Sign Nostr events with Schnorr signatures +- ✅ **NIP-44 Encryption** - v2 `encrypt`/`decrypt` for NIP-17/NIP-59 gift-wrapped DMs (key never leaves the background) - ✅ **Trust Management** - Per-origin permissions with auto-approval - ✅ **Beautiful UI** - Soft gradients, smooth animations - ✅ **64-char Hex Keys** - Proper did:nostr format compatibility @@ -261,6 +262,19 @@ const signed = await window.nostr.signEvent({ }) ``` +### `window.nostr.nip44.encrypt(pubkey, plaintext)` / `window.nostr.nip44.decrypt(pubkey, ciphertext)` + +NIP-44 (v2) encryption, used by NIP-17 / NIP-59 gift-wrapped direct messages. +The private key never leaves the background service worker — encryption is +performed there and only the resulting base64 payload (or decrypted plaintext) +crosses to the page, mirroring the existing signing message path. + +```javascript +const peer = '<64-char hex pubkey>' +const payload = await window.nostr.nip44.encrypt(peer, 'hello') +const plaintext = await window.nostr.nip44.decrypt(peer, payload) +``` + ### `window.nostr.getRelays()` Returns relay configuration (coming soon). diff --git a/package-lock.json b/package-lock.json index 8fe28a2..c9ffaeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "podkey", - "version": "0.0.2", + "version": "0.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "podkey", - "version": "0.0.2", - "license": "MIT", + "version": "0.0.7", + "license": "AGPL-3.0", "dependencies": { + "@noble/ciphers": "^2.2.0", "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^3.0.0" }, @@ -563,6 +564,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", diff --git a/package.json b/package.json index 3e04ce5..fafbc50 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "echo 'Load extension from chrome://extensions in developer mode'", "build": "npm run bundle", "bundle": "node scripts/bundle.js", - "test": "node --test test/**/*.test.js", + "test": "node --test test/*.test.js", "lint": "eslint src/" }, "keywords": [ @@ -34,6 +34,7 @@ }, "homepage": "https://github.com/JavaScriptSolidServer/podkey#readme", "dependencies": { + "@noble/ciphers": "^2.2.0", "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^3.0.0" }, @@ -44,4 +45,4 @@ "engines": { "node": ">=18.0.0" } -} \ No newline at end of file +} diff --git a/popup/approve.css b/popup/approve.css new file mode 100644 index 0000000..3f042c9 --- /dev/null +++ b/popup/approve.css @@ -0,0 +1,252 @@ +/** + * Podkey - Signing-approval consent prompt + * The most security-critical surface: a site is asking to act as the user. + * Goal: unambiguous, calm, hard to mis-click. Deny is the safe default. + * Inherits design tokens from popup.css (loaded first). + */ + +body { + width: 380px; + min-height: 0; +} + +.approve { + display: flex; + flex-direction: column; + padding: 16px 16px 14px; +} + +/* ---- Header ---- */ +.approve-head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 14px; +} + +.approve-shield { + width: 36px; + height: 36px; + flex: none; + display: grid; + place-items: center; + border-radius: 10px; + background: var(--accent-soft); + color: var(--accent); + border: 1px solid var(--border); +} + +.approve-shield svg { + width: 20px; + height: 20px; +} + +.approve-title { + font-size: 16px; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--text); +} + +.approve-sub { + font-size: 11.5px; + color: var(--text-muted); + margin-top: 1px; +} + +/* ---- Origin block (the spoof-resistant focal point) ---- */ +.origin-block { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px 14px; + margin-bottom: 12px; + box-shadow: var(--shadow-sm); +} + +.origin-eyebrow { + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-faint); + margin-bottom: 6px; +} + +.origin-badge { + display: block; + font-family: var(--mono); + font-size: 14px; + font-weight: 600; + line-height: 1.4; + color: var(--text); + background: var(--surface-inset); + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + padding: 9px 11px; + /* Long/hostile origins wrap and stay contained; cannot push layout. */ + word-break: break-all; + overflow-wrap: anywhere; + max-height: 84px; + overflow-y: auto; +} + +/* ---- Action ---- */ +.action-row { + display: flex; + align-items: flex-start; + gap: 9px; + margin-bottom: 12px; + font-size: 13.5px; + color: var(--text); +} + +.action-row .action-icon { + width: 18px; + height: 18px; + flex: none; + margin-top: 1px; + color: var(--text-muted); +} + +.action-row .action-text { + font-weight: 600; +} + +/* ---- Preview ---- */ +.preview-wrap { + margin-bottom: 12px; +} + +.preview-label { + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-faint); + margin-bottom: 6px; +} + +.event-preview { + background: var(--surface-inset); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 11px 12px; + font-family: var(--mono); + font-size: 11.5px; + line-height: 1.5; + color: var(--text-muted); + max-height: 150px; + overflow: auto; + white-space: pre; + word-break: normal; + tab-size: 2; +} + +/* ---- Trust warning ---- */ +.trust-note { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 11.5px; + line-height: 1.45; + color: var(--warn); + margin-bottom: 14px; + padding: 9px 11px; + background: var(--warn-soft); + border: 1px solid var(--warn-border); + border-radius: var(--radius-sm); +} + +.trust-note svg { + width: 15px; + height: 15px; + flex: none; + margin-top: 1px; +} + +/* ---- Buttons ---- */ +.btn-row { + display: flex; + gap: 10px; +} + +.btn-row .btn { + flex: 1; + margin-bottom: 0; +} + +/* Deny = safe default: solid, visually settled, autofocused. */ +.btn-deny { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border-strong); +} + +.btn-deny:hover { + background: var(--surface-2); +} + +.btn-deny:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-ring); +} + +/* Approve = deliberate: outlined danger-tone, not pre-focused. */ +.btn-approve { + background: transparent; + color: var(--danger); + border: 1.5px solid var(--danger-border); +} + +.btn-approve:hover { + background: var(--danger-soft); + border-color: var(--danger); +} + +.btn-approve:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.3); +} + +/* ---- Countdown ---- */ +.countdown { + margin-top: 12px; +} + +.countdown-text { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 11px; + color: var(--text-faint); + margin-bottom: 7px; +} + +.countdown-text strong { + color: var(--text-muted); + font-variant-numeric: tabular-nums; + font-weight: 700; +} + +.timeout-bar { + height: 4px; + background: var(--border); + border-radius: 999px; + overflow: hidden; +} + +.timeout-bar-inner { + height: 100%; + width: 100%; + background: var(--text-faint); + border-radius: 999px; + transform-origin: left center; + /* Width is driven from JS for an accurate, synced countdown. */ + transition: background 0.4s ease; +} + +.timeout-bar-inner.urgent { + background: var(--danger); +} diff --git a/popup/approve.html b/popup/approve.html new file mode 100644 index 0000000..29b0099 --- /dev/null +++ b/popup/approve.html @@ -0,0 +1,68 @@ + + + + + + Podkey — Signing Request + + + + +
+
+ +
+
Signing request
+
Review carefully before approving
+
+
+ +
+
Requested by
+ +
+ +
+ + +
+ +
+
Payload
+

+      
+ +
+ + Approving lets this site act with your key as if it were you. +
+ +
+ + +
+ +
+
+ Auto-denies in 60s +
+
+
+
+
+
+ + + diff --git a/popup/approve.js b/popup/approve.js new file mode 100644 index 0000000..2c8f5ab --- /dev/null +++ b/popup/approve.js @@ -0,0 +1,102 @@ +/** + * Podkey - Signing Approval Popup + * Presents signing requests to the user for explicit approval/denial. + * + * Security contract (must stay byte-equivalent in effect): + * - Read requestId/origin/action/preview from the query string. + * - On a choice: chrome.runtime.sendMessage( + * { type: 'APPROVE_SIGNING', requestId, approved: }). + * - Closing the popup (beforeunload) sends approved:false (closing = deny). + * - A 60-second auto-deny countdown denies if the user does nothing. + * + * The displayed origin and payload come from web pages and are treated as + * hostile strings: only ever written via textContent, never innerHTML. + */ + +const APPROVAL_TIMEOUT_MS = 60_000; + +const params = new URLSearchParams(window.location.search); +const requestId = params.get('id'); +const origin = params.get('origin') || 'Unknown'; +const action = params.get('action') || 'sign'; +const preview = params.get('preview') || ''; + +// One-shot guard: the decision is sent exactly once, whichever path fires +// first (button click, auto-deny timeout, or window close). +let decisionSent = false; + +function sendDecision(approved) { + if (decisionSent) return; + decisionSent = true; + chrome.runtime.sendMessage({ + type: 'APPROVE_SIGNING', + requestId, + approved + }); +} + +// ---- Populate UI (textContent only — never innerHTML for untrusted data) ---- + +document.getElementById('origin').textContent = origin; +document.getElementById('action').textContent = action; + +const previewEl = document.getElementById('preview'); +const previewWrap = document.getElementById('previewWrap'); +if (preview) { + try { + // Pretty-print JSON payloads for readability; fall back to raw text. + const parsed = JSON.parse(preview); + previewEl.textContent = JSON.stringify(parsed, null, 2); + } catch { + previewEl.textContent = preview; + } +} else { + previewWrap.style.display = 'none'; +} + +// ---- Buttons ---- + +document.getElementById('approve').addEventListener('click', () => { + sendDecision(true); + window.close(); +}); + +document.getElementById('deny').addEventListener('click', () => { + sendDecision(false); + window.close(); +}); + +// Closing the popup without choosing is a denial. +window.addEventListener('beforeunload', () => { + sendDecision(false); +}); + +// ---- 60-second auto-deny countdown ---- + +const countdownEl = document.getElementById('countdown'); +const barEl = document.getElementById('timeoutBar'); +const startedAt = Date.now(); + +function tickCountdown() { + if (decisionSent) return; + + const elapsed = Date.now() - startedAt; + const remainingMs = Math.max(0, APPROVAL_TIMEOUT_MS - elapsed); + const remainingSec = Math.ceil(remainingMs / 1000); + const fraction = remainingMs / APPROVAL_TIMEOUT_MS; + + countdownEl.textContent = String(remainingSec); + barEl.style.width = `${(fraction * 100).toFixed(2)}%`; + barEl.classList.toggle('urgent', remainingSec <= 10); + + if (remainingMs <= 0) { + // Time is up: deny and close. + sendDecision(false); + window.close(); + return; + } + + requestAnimationFrame(tickCountdown); +} + +requestAnimationFrame(tickCountdown); diff --git a/popup/popup.css b/popup/popup.css index 6db5dfe..3408f7d 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -1,8 +1,100 @@ /** * Podkey - Popup Styles - * Beautiful soft light theme with gradients + * Calm, modern, trustworthy. Slate neutrals with a single trust-blue accent; + * semantic green/red reserved for safe/destructive actions only. + * Dark mode via prefers-color-scheme. No frameworks, no dependencies. */ +:root { + /* Surfaces */ + --bg: #f6f7f9; + --surface: #ffffff; + --surface-2: #f1f3f6; + --surface-inset: #f8fafc; + + /* Text */ + --text: #1e293b; + --text-muted: #64748b; + --text-faint: #94a3b8; + + /* Borders */ + --border: #e2e8f0; + --border-strong: #cbd5e1; + + /* Accent (trust blue) */ + --accent: #2563eb; + --accent-hover: #1d4ed8; + --accent-soft: #eff6ff; + --accent-ring: rgba(37, 99, 235, 0.25); + + /* Semantic */ + --safe: #16a34a; + --safe-hover: #15803d; + --safe-soft: #f0fdf4; + --safe-border: #bbf7d0; + + --danger: #dc2626; + --danger-hover: #b91c1c; + --danger-soft: #fef2f2; + --danger-border: #fecaca; + + --warn: #b45309; + --warn-soft: #fffbeb; + --warn-border: #fde68a; + + /* Shape */ + --radius: 12px; + --radius-sm: 8px; + --radius-lg: 16px; + --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow: 0 4px 16px rgba(15, 23, 42, 0.08); + --shadow-lg: 0 10px 30px rgba(15, 23, 42, 0.12); + + --mono: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', + monospace; + --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', + Arial, sans-serif; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0e131b; + --surface: #161c26; + --surface-2: #1c232f; + --surface-inset: #0f151d; + + --text: #e6eaf0; + --text-muted: #9aa6b6; + --text-faint: #6b7888; + + --border: #2a3340; + --border-strong: #3a4452; + + --accent: #5b8def; + --accent-hover: #74a0f4; + --accent-soft: #16223a; + --accent-ring: rgba(91, 141, 239, 0.35); + + --safe: #4ade80; + --safe-hover: #6ee79a; + --safe-soft: #122418; + --safe-border: #1f4d30; + + --danger: #f87171; + --danger-hover: #fca5a5; + --danger-soft: #2a1414; + --danger-border: #5a2626; + + --warn: #fbbf24; + --warn-soft: #2a2310; + --warn-border: #5a4a1a; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow: 0 4px 16px rgba(0, 0, 0, 0.45); + --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.55); + } +} + * { box-sizing: border-box; margin: 0; @@ -10,205 +102,321 @@ } body { - width: 420px; - min-height: 500px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; - background: linear-gradient(135deg, #fef5ff 0%, #f0f9ff 50%, #fef3c7 100%); - color: #334155; - padding: 0; + width: 380px; + min-height: 480px; + font-family: var(--sans); + background: var(--bg); + color: var(--text); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; } .screen { - padding: 20px; + padding: 18px 18px 14px; } -/* Header */ +/* ---- Header ---- */ .header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 18px; +} + +.header.center { + flex-direction: column; text-align: center; - margin-bottom: 24px; - padding-bottom: 20px; - border-bottom: 2px solid rgba(168, 85, 247, 0.2); + gap: 8px; + margin-bottom: 20px; } -.logo { - font-size: 64px; - margin-bottom: 12px; - filter: drop-shadow(0 4px 8px rgba(168, 85, 247, 0.15)); +.brand-mark { + width: 36px; + height: 36px; + flex: none; + display: grid; + place-items: center; + border-radius: 10px; + background: var(--accent-soft); + color: var(--accent); + border: 1px solid var(--border); } -.logo-small { - font-size: 32px; - display: inline-block; - margin-right: 8px; +.brand-mark svg { + width: 20px; + height: 20px; +} + +.brand-mark.lg { + width: 56px; + height: 56px; + border-radius: 16px; + margin-bottom: 2px; +} + +.brand-mark.lg svg { + width: 30px; + height: 30px; } h1 { - font-size: 24px; + font-size: 18px; font-weight: 700; - background: linear-gradient(135deg, #7c3aed 0%, #3b82f6 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: 6px; + letter-spacing: -0.01em; + color: var(--text); } h2 { - font-size: 20px; - color: #6b21a8; - margin-bottom: 8px; + font-size: 17px; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--text); } .subtitle { - font-size: 13px; - color: #9333ea; - font-weight: 500; + font-size: 12.5px; + color: var(--text-muted); + line-height: 1.45; + max-width: 30ch; +} + +.header.center .subtitle { + margin: 0 auto; } -/* Sections */ +/* ---- Sections / cards ---- */ .section { - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(10px); - border: 1px solid rgba(168, 85, 247, 0.15); - border-radius: 16px; - padding: 16px; - margin-bottom: 16px; - box-shadow: 0 4px 12px rgba(168, 85, 247, 0.08); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + margin-bottom: 12px; + box-shadow: var(--shadow-sm); } .section-title { - font-size: 14px; - font-weight: 600; - color: #7c3aed; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); margin-bottom: 12px; } -/* Buttons */ +/* ---- Buttons ---- */ .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; width: 100%; - padding: 14px; - border: none; - border-radius: 12px; - font-size: 15px; + padding: 11px 14px; + border: 1px solid transparent; + border-radius: var(--radius-sm); + font-family: inherit; + font-size: 14px; font-weight: 600; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease, + transform 0.05s ease, box-shadow 0.15s ease; margin-bottom: 10px; } +.btn:last-child { + margin-bottom: 0; +} + +.btn svg { + width: 16px; + height: 16px; + flex: none; +} + +.btn:active { + transform: translateY(1px); +} + +.btn:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-ring); +} + .btn-primary { - background: linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%); - color: white; - box-shadow: 0 4px 12px rgba(124, 58, 237, 0.25); + background: var(--accent); + color: #fff; + box-shadow: var(--shadow-sm); } .btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(124, 58, 237, 0.35); + background: var(--accent-hover); } .btn-secondary { - background: linear-gradient(135deg, #e0e7ff 0%, #dbeafe 100%); - color: #6b21a8; - border: 1px solid #c7d2fe; + background: var(--surface); + color: var(--text); + border-color: var(--border-strong); } .btn-secondary:hover { - background: linear-gradient(135deg, #c7d2fe 0%, #bfdbfe 100%); - transform: translateY(-1px); + background: var(--surface-2); +} + +.btn[disabled] { + opacity: 0.55; + cursor: not-allowed; +} + +.btn[disabled]:active { + transform: none; +} + +/* ---- Identity ---- */ +.identity-section .section-title { + margin-bottom: 10px; +} + +.identity-field { + margin-bottom: 12px; +} + +.identity-field:last-of-type { + margin-bottom: 14px; +} + +.identity-label { + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-faint); + margin-bottom: 5px; +} + +.identity-value { + font-family: var(--mono); + font-size: 11.5px; + line-height: 1.55; + color: var(--text); + background: var(--surface-inset); + border: 1px solid var(--border); + padding: 9px 11px; + border-radius: var(--radius-sm); + word-break: break-all; } .btn-copy { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; width: 100%; - padding: 10px; - margin-top: 12px; - background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 100%); - border: 1px solid #fbbf24; - border-radius: 8px; - color: #92400e; - font-size: 13px; + padding: 9px; + background: var(--surface); + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + color: var(--text); + font-family: inherit; + font-size: 12.5px; font-weight: 600; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease, color 0.15s ease; } -.btn-copy:hover { - background: linear-gradient(135deg, #fde68a 0%, #fbbf24 100%); - transform: translateY(-1px); +.btn-copy svg { + width: 14px; + height: 14px; } -/* Identity Card */ -.identity-card { - background: linear-gradient(135deg, #fef3c7 0%, #dbeafe 100%); - border: 1px solid #bfdbfe; - border-radius: 12px; - padding: 16px; +.btn-copy:hover { + background: var(--surface-2); } -.identity-label { - font-size: 11px; - font-weight: 700; - color: #7c2d12; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-top: 12px; - margin-bottom: 6px; +.btn-copy.copied { + color: var(--safe); + border-color: var(--safe-border); + background: var(--safe-soft); } -.identity-label:first-child { - margin-top: 0; +/* ---- Session/lock pill ---- */ +.lock-pill { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 9px; + border-radius: 999px; + font-size: 10.5px; + font-weight: 700; + text-transform: none; + letter-spacing: 0; + background: var(--safe-soft); + color: var(--safe); + border: 1px solid var(--safe-border); } -.identity-value { - font-family: 'Monaco', 'Menlo', 'Courier New', monospace; - font-size: 11px; - color: #475569; - background: rgba(255, 255, 255, 0.8); - padding: 8px 10px; - border-radius: 6px; - word-break: break-all; - line-height: 1.5; +.lock-pill svg { + width: 11px; + height: 11px; } -/* Input Fields */ +/* ---- Inputs ---- */ label { display: block; - font-size: 13px; + font-size: 12.5px; font-weight: 600; - color: #6b21a8; - margin-bottom: 8px; + color: var(--text); + margin-bottom: 7px; } textarea { width: 100%; - padding: 12px; - border: 2px solid #e9d5ff; - border-radius: 8px; - font-family: 'Monaco', 'Menlo', monospace; + padding: 11px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + font-family: var(--mono); font-size: 12px; + line-height: 1.5; resize: vertical; - background: rgba(255, 255, 255, 0.9); - color: #334155; - transition: border-color 0.2s; + background: var(--surface-inset); + color: var(--text); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +textarea::placeholder { + color: var(--text-faint); } textarea:focus { outline: none; - border-color: #a78bfa; - box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.1); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); } .hint { - font-size: 11px; - color: #dc2626; - margin-top: 8px; - margin-bottom: 16px; - padding: 8px 12px; - background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%); - border: 1px solid #fca5a5; - border-radius: 6px; -} - -/* Actions */ + display: flex; + align-items: flex-start; + gap: 7px; + font-size: 11.5px; + line-height: 1.45; + color: var(--warn); + margin: 10px 0 0; + padding: 9px 11px; + background: var(--warn-soft); + border: 1px solid var(--warn-border); + border-radius: var(--radius-sm); +} + +.hint svg { + width: 14px; + height: 14px; + flex: none; + margin-top: 1px; +} + +/* ---- Actions row ---- */ .actions { display: flex; gap: 10px; @@ -219,23 +427,38 @@ textarea:focus { margin-bottom: 0; } -/* Settings */ +/* ---- Settings ---- */ .settings-item { display: flex; align-items: center; justify-content: space-between; - padding: 12px 0; - color: #6b21a8; - font-weight: 500; - font-size: 14px; + gap: 12px; } -/* Toggle Switch */ +.settings-text { + min-width: 0; +} + +.settings-label { + font-size: 13.5px; + font-weight: 600; + color: var(--text); +} + +.settings-desc { + font-size: 11.5px; + color: var(--text-muted); + line-height: 1.4; + margin-top: 2px; +} + +/* Toggle */ .toggle { position: relative; display: inline-block; - width: 50px; - height: 26px; + width: 42px; + height: 24px; + flex: none; } .toggle input { @@ -246,147 +469,174 @@ textarea:focus { .slider { position: absolute; + inset: 0; cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: #cbd5e1; - transition: 0.3s; - border-radius: 26px; + background: var(--border-strong); + transition: background 0.2s ease; + border-radius: 999px; } .slider:before { position: absolute; - content: ""; - height: 20px; - width: 20px; + content: ''; + height: 18px; + width: 18px; left: 3px; bottom: 3px; - background: white; - transition: 0.3s; + background: #fff; + transition: transform 0.2s ease; border-radius: 50%; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: var(--shadow-sm); } input:checked + .slider { - background: linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%); + background: var(--accent); } input:checked + .slider:before { - transform: translateX(24px); + transform: translateX(18px); +} + +input:focus-visible + .slider { + box-shadow: 0 0 0 3px var(--accent-ring); } -/* Trusted Sites */ +/* ---- Trusted sites ---- */ .trusted-list { - max-height: 180px; + max-height: 168px; overflow-y: auto; + margin: -2px; + padding: 2px; } .trusted-item { display: flex; align-items: center; justify-content: space-between; - padding: 10px 12px; - margin: 6px 0; - background: linear-gradient(135deg, #fef3c7 0%, #e0f2fe 100%); - border: 1px solid #bfdbfe; - border-radius: 8px; - font-size: 12px; + gap: 10px; + padding: 9px 11px; + margin-bottom: 7px; + background: var(--surface-inset); + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.trusted-item:last-child { + margin-bottom: 0; } .trusted-origin { - font-family: monospace; - color: #475569; + font-family: var(--mono); + font-size: 11.5px; + color: var(--text); flex: 1; - margin-right: 8px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .btn-remove { + flex: none; padding: 4px 10px; - background: linear-gradient(135deg, #fecaca 0%, #fca5a5 100%); - border: 1px solid #f87171; - border-radius: 6px; - color: #991b1b; + background: transparent; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + color: var(--text-muted); + font-family: inherit; font-size: 11px; font-weight: 600; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; } .btn-remove:hover { - background: linear-gradient(135deg, #fca5a5 0%, #f87171 100%); - transform: translateY(-1px); + background: var(--danger-soft); + color: var(--danger); + border-color: var(--danger-border); +} + +.btn-remove:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-ring); } .empty-state { text-align: center; - padding: 32px 16px; - color: #a78bfa; - font-size: 13px; - font-style: italic; + padding: 22px 16px; + color: var(--text-faint); + font-size: 12.5px; } -/* Info Box */ +/* ---- Info box ---- */ .info-box { - background: linear-gradient(135deg, #f0fdf4 0%, #dbeafe 100%); - border: 1px solid #86efac; - border-radius: 12px; - padding: 16px; - margin-top: 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px; + margin-top: 4px; + box-shadow: var(--shadow-sm); } -.info-box p { - font-size: 12px; - line-height: 1.6; - color: #166534; - margin-bottom: 8px; +.info-box-title { + font-size: 12.5px; + font-weight: 700; + color: var(--text); + margin-bottom: 6px; } -.info-box p:last-child { - margin-bottom: 0; +.info-box p { + font-size: 12px; + line-height: 1.55; + color: var(--text-muted); } .info-box strong { - color: #15803d; + color: var(--text); font-weight: 600; } -/* Footer */ +/* ---- Footer ---- */ .footer { text-align: center; - padding: 16px; + padding: 12px 0 2px; font-size: 12px; - color: #9333ea; + color: var(--text-muted); } .footer a { - color: #7c3aed; + color: var(--accent); text-decoration: none; font-weight: 600; - transition: color 0.2s; } .footer a:hover { - color: #6b21a8; text-decoration: underline; } -/* Scrollbar Styling */ -.trusted-list::-webkit-scrollbar { - width: 6px; +.footer .sep { + margin: 0 6px; + color: var(--text-faint); +} + +/* ---- Scrollbars ---- */ +.trusted-list::-webkit-scrollbar, +.event-preview::-webkit-scrollbar { + width: 8px; } -.trusted-list::-webkit-scrollbar-track { - background: rgba(168, 85, 247, 0.05); - border-radius: 3px; +.trusted-list::-webkit-scrollbar-track, +.event-preview::-webkit-scrollbar-track { + background: transparent; } -.trusted-list::-webkit-scrollbar-thumb { - background: linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%); - border-radius: 3px; +.trusted-list::-webkit-scrollbar-thumb, +.event-preview::-webkit-scrollbar-thumb { + background: var(--border-strong); + border-radius: 999px; } -.trusted-list::-webkit-scrollbar-thumb:hover { - background: linear-gradient(135deg, #7c3aed 0%, #6b21a8 100%); +.trusted-list::-webkit-scrollbar-thumb:hover, +.event-preview::-webkit-scrollbar-thumb:hover { + background: var(--text-faint); } diff --git a/popup/popup.html b/popup/popup.html index 5ae2fed..ecfc185 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -3,14 +3,21 @@ - Podkey - Your Nostr Identity + Podkey — Your Nostr Identity