hd-wallet-ui 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-wallet-ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "HD Wallet modal UI — login, keys, identity, trust map, and security bond. Attach to any button in your app.",
5
5
  "type": "module",
6
6
  "main": "src/app.js",
@@ -34,7 +34,7 @@
34
34
  "bip39": "^3.1.0",
35
35
  "buffer": "^6.0.3",
36
36
  "flatbuffers": "^25.9.23",
37
- "hd-wallet-wasm": "file:../wasm",
37
+ "hd-wallet-wasm": "^1.1.1",
38
38
  "qrcode": "^1.5.3",
39
39
  "vcard-cryptoperson": "^1.1.10"
40
40
  },
@@ -55,5 +55,13 @@
55
55
  "ethereum",
56
56
  "solana"
57
57
  ],
58
- "license": "Apache-2.0"
58
+ "license": "Apache-2.0",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/DigitalArsenal/hd-wallet-wasm.git"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/DigitalArsenal/hd-wallet-wasm/issues"
65
+ },
66
+ "homepage": "https://digitalarsenal.github.io/hd-wallet-wasm/"
59
67
  }
package/src/app.js CHANGED
@@ -19,6 +19,10 @@ import QRCode from 'qrcode';
19
19
  import { Buffer } from 'buffer';
20
20
  import { createV3 } from 'vcard-cryptoperson';
21
21
 
22
+ // SpaceDataStandards EME (Encrypted Message Envelope)
23
+ import { EME, EMET } from '@sds/lib/js/ts/EME/EME.js';
24
+ import * as flatbuffers from 'flatbuffers';
25
+
22
26
  // Make Buffer available globally for various crypto libraries
23
27
  window.Buffer = Buffer;
24
28
 
@@ -2772,14 +2776,17 @@ function setupMainAppHandlers() {
2772
2776
  $('hd-coin')?.addEventListener('change', () => {
2773
2777
  updatePathDisplay();
2774
2778
  deriveAndDisplayAddress();
2779
+ updateEncryptionTab();
2775
2780
  });
2776
2781
  $('hd-account')?.addEventListener('input', () => {
2777
2782
  updatePathDisplay();
2778
2783
  deriveAndDisplayAddress();
2784
+ updateEncryptionTab();
2779
2785
  });
2780
2786
  $('hd-index')?.addEventListener('input', () => {
2781
2787
  updatePathDisplay();
2782
2788
  deriveAndDisplayAddress();
2789
+ updateEncryptionTab();
2783
2790
  });
2784
2791
 
2785
2792
  // PKI clear keys
@@ -3129,6 +3136,284 @@ function setupTrustHandlers() {
3129
3136
  // Expose start/stop for login/logout
3130
3137
  state._startTrustScanning = startTrustScanning;
3131
3138
  state._stopTrustScanning = stopTrustScanning;
3139
+
3140
+ // =========================================================================
3141
+ // Encryption Tab Handlers (ECIES: ECDH + HKDF + AES-256-GCM)
3142
+ // =========================================================================
3143
+
3144
+ function updateEncryptionTab() {
3145
+ if (!state.hdRoot || !state.hdWalletModule) return;
3146
+ const coin = $('hd-coin')?.value || '0';
3147
+ const account = $('hd-account')?.value || '0';
3148
+ const index = $('hd-index')?.value || '0';
3149
+ const encPath = buildEncryptionPath(coin, account, index);
3150
+ const encKey = deriveHDKey(encPath);
3151
+ const pubKey = encKey.publicKey();
3152
+ const pubHex = toHexCompact(pubKey);
3153
+
3154
+ const senderPubEl = $('encrypt-sender-pubkey');
3155
+ const senderPathEl = $('encrypt-sender-path');
3156
+ if (senderPubEl) senderPubEl.textContent = pubHex;
3157
+ if (senderPathEl) senderPathEl.textContent = encPath;
3158
+
3159
+ const encryptBtn = $('encrypt-btn');
3160
+ if (encryptBtn) encryptBtn.disabled = false;
3161
+ }
3162
+
3163
+ // Update encryption tab when it becomes active or HD controls change
3164
+ $qa('.modal-tab[data-modal-tab="encryption-tab-content"]').forEach(tab => {
3165
+ tab.addEventListener('click', () => {
3166
+ if (state.hdRoot) updateEncryptionTab();
3167
+ });
3168
+ });
3169
+
3170
+ // "Self" button - fill recipient with own public key for testing
3171
+ $('encrypt-use-self')?.addEventListener('click', () => {
3172
+ const senderPub = $('encrypt-sender-pubkey')?.textContent;
3173
+ if (senderPub && senderPub !== '--') {
3174
+ $('encrypt-recipient-pubkey').value = senderPub;
3175
+ }
3176
+ });
3177
+
3178
+ // ---- EME state for current encryption result ----
3179
+ let currentEME = null; // EMET instance
3180
+ let currentFormat = 'json';
3181
+
3182
+ function emeToJSON(eme) {
3183
+ return JSON.stringify({
3184
+ ENCRYPTED_BLOB: eme.ENCRYPTED_BLOB,
3185
+ EPHEMERAL_PUBLIC_KEY: eme.EPHEMERAL_PUBLIC_KEY,
3186
+ MAC: eme.MAC,
3187
+ NONCE: eme.NONCE,
3188
+ TAG: eme.TAG,
3189
+ IV: eme.IV,
3190
+ SALT: eme.SALT,
3191
+ PUBLIC_KEY_IDENTIFIER: eme.PUBLIC_KEY_IDENTIFIER,
3192
+ CIPHER_SUITE: eme.CIPHER_SUITE,
3193
+ KDF_PARAMETERS: eme.KDF_PARAMETERS,
3194
+ ENCRYPTION_ALGORITHM_PARAMETERS: eme.ENCRYPTION_ALGORITHM_PARAMETERS,
3195
+ }, null, 2);
3196
+ }
3197
+
3198
+ function emeToFlatBuffer(eme) {
3199
+ const builder = new flatbuffers.Builder(1);
3200
+ const packed = eme.pack(builder);
3201
+ builder.finishSizePrefixed(packed, '$EME');
3202
+ return builder.asUint8Array();
3203
+ }
3204
+
3205
+ function emeToFlatBufferBase64(eme) {
3206
+ return Buffer.from(emeToFlatBuffer(eme)).toString('base64');
3207
+ }
3208
+
3209
+ function updateBundleDisplay() {
3210
+ if (!currentEME) return;
3211
+ const textarea = $('encrypt-bundle');
3212
+ if (!textarea) return;
3213
+ if (currentFormat === 'json') {
3214
+ textarea.value = emeToJSON(currentEME);
3215
+ } else {
3216
+ textarea.value = emeToFlatBufferBase64(currentEME);
3217
+ }
3218
+ }
3219
+
3220
+ // Format toggle buttons
3221
+ $qa('.encrypt-fmt-btn').forEach(btn => {
3222
+ btn.addEventListener('click', () => {
3223
+ $qa('.encrypt-fmt-btn').forEach(b => b.classList.remove('active'));
3224
+ btn.classList.add('active');
3225
+ currentFormat = btn.dataset.format;
3226
+ const label = $('encrypt-format-label');
3227
+ if (label) label.textContent = currentFormat === 'json'
3228
+ ? 'EME (Encrypted Message Envelope) — SpaceDataStandards.org'
3229
+ : 'EME FlatBuffer binary (base64-encoded)';
3230
+ updateBundleDisplay();
3231
+ });
3232
+ });
3233
+
3234
+ // Encrypt button
3235
+ $('encrypt-btn')?.addEventListener('click', () => {
3236
+ const w = state.hdWalletModule;
3237
+ if (!w || !state.hdRoot) return;
3238
+
3239
+ const recipientHex = $('encrypt-recipient-pubkey')?.value?.trim();
3240
+ const plainStr = $('encrypt-plaintext')?.value;
3241
+ if (!recipientHex || !plainStr) {
3242
+ alert('Please enter both a recipient public key and a message.');
3243
+ return;
3244
+ }
3245
+
3246
+ try {
3247
+ const coin = $('hd-coin')?.value || '0';
3248
+ const account = $('hd-account')?.value || '0';
3249
+ const index = $('hd-index')?.value || '0';
3250
+ const encPath = buildEncryptionPath(coin, account, index);
3251
+ const senderKey = deriveHDKey(encPath);
3252
+ const senderPriv = senderKey.privateKey();
3253
+ const senderPub = senderKey.publicKey();
3254
+
3255
+ // Parse recipient public key from hex
3256
+ const recipientPub = new Uint8Array(recipientHex.match(/.{1,2}/g).map(b => parseInt(b, 16)));
3257
+
3258
+ // 1. ECDH shared secret
3259
+ const shared = w.curves.secp256k1.ecdh(senderPriv, recipientPub);
3260
+
3261
+ // 2. HKDF: derive 32-byte AES key from shared secret
3262
+ const salt = w.utils.getRandomBytes(32);
3263
+ const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
3264
+ const aesKey = w.utils.hkdf(shared, salt, info, 32);
3265
+
3266
+ // 3. AES-256-GCM encrypt
3267
+ const iv = w.utils.generateIv();
3268
+ const plaintext = new TextEncoder().encode(plainStr);
3269
+ const { ciphertext, tag } = w.utils.aesGcm.encrypt(aesKey, plaintext, iv);
3270
+
3271
+ // Display field-level results
3272
+ $('encrypt-out-ciphertext').textContent = toHexCompact(ciphertext);
3273
+ $('encrypt-out-tag').textContent = toHexCompact(tag);
3274
+ $('encrypt-out-iv').textContent = toHexCompact(iv);
3275
+ $('encrypt-out-salt').textContent = toHexCompact(salt);
3276
+ $('encrypt-out-sender-pub').textContent = toHexCompact(senderPub);
3277
+ $('encrypt-output-section').style.display = 'block';
3278
+
3279
+ // Build EME (Encrypted Message Envelope) standard object
3280
+ currentEME = new EMET(
3281
+ Array.from(ciphertext), // ENCRYPTED_BLOB
3282
+ toHexCompact(senderPub), // EPHEMERAL_PUBLIC_KEY
3283
+ null, // MAC (not used, tag covers it)
3284
+ null, // NONCE (we use IV field instead)
3285
+ toHexCompact(tag), // TAG
3286
+ toHexCompact(iv), // IV
3287
+ toHexCompact(salt), // SALT
3288
+ null, // PUBLIC_KEY_IDENTIFIER
3289
+ 'aes-256-gcm', // CIPHER_SUITE
3290
+ 'hkdf-sha256', // KDF_PARAMETERS
3291
+ 'secp256k1', // ENCRYPTION_ALGORITHM_PARAMETERS
3292
+ );
3293
+
3294
+ updateBundleDisplay();
3295
+
3296
+ // Clear any previous decrypt result
3297
+ $('decrypt-result').style.display = 'none';
3298
+ } catch (err) {
3299
+ console.error('Encryption failed:', err);
3300
+ alert('Encryption failed: ' + err.message);
3301
+ }
3302
+ });
3303
+
3304
+ // Copy bundle
3305
+ $('encrypt-copy-bundle')?.addEventListener('click', () => {
3306
+ const bundle = $('encrypt-bundle')?.value;
3307
+ if (bundle) {
3308
+ navigator.clipboard.writeText(bundle).catch(() => {});
3309
+ }
3310
+ });
3311
+
3312
+ // Download bundle
3313
+ $('encrypt-download-bundle')?.addEventListener('click', () => {
3314
+ if (!currentEME) return;
3315
+ let blob, filename;
3316
+ if (currentFormat === 'json') {
3317
+ blob = new Blob([emeToJSON(currentEME)], { type: 'application/json' });
3318
+ filename = 'message.eme.json';
3319
+ } else {
3320
+ const buf = emeToFlatBuffer(currentEME);
3321
+ blob = new Blob([buf], { type: 'application/octet-stream' });
3322
+ filename = 'message.eme.fbs';
3323
+ }
3324
+ const url = URL.createObjectURL(blob);
3325
+ const a = document.createElement('a');
3326
+ a.href = url;
3327
+ a.download = filename;
3328
+ a.click();
3329
+ URL.revokeObjectURL(url);
3330
+ });
3331
+
3332
+ // Parse EME from input (JSON or base64 FlatBuffer)
3333
+ function parseEMEPayload(input) {
3334
+ const trimmed = input.trim();
3335
+ // Try JSON first
3336
+ if (trimmed.startsWith('{')) {
3337
+ return JSON.parse(trimmed);
3338
+ }
3339
+ // Try base64 FlatBuffer (size-prefixed)
3340
+ const buf = new Uint8Array(Buffer.from(trimmed, 'base64'));
3341
+ const bb = new flatbuffers.ByteBuffer(buf);
3342
+ const root = EME.getSizePrefixedRootAsEME(bb);
3343
+ const eme = root.unpack();
3344
+ return eme;
3345
+ }
3346
+
3347
+ // Decrypt button
3348
+ $('decrypt-btn')?.addEventListener('click', () => {
3349
+ const w = state.hdWalletModule;
3350
+ if (!w || !state.hdRoot) return;
3351
+
3352
+ const payloadStr = $('decrypt-payload')?.value?.trim();
3353
+ if (!payloadStr) {
3354
+ alert('Paste an EME payload to decrypt.');
3355
+ return;
3356
+ }
3357
+
3358
+ try {
3359
+ const payload = parseEMEPayload(payloadStr);
3360
+ const fromHex = (h) => new Uint8Array(h.match(/.{1,2}/g).map(b => parseInt(b, 16)));
3361
+
3362
+ const senderPub = fromHex(payload.EPHEMERAL_PUBLIC_KEY);
3363
+ const tag = fromHex(payload.TAG);
3364
+ const iv = fromHex(payload.IV);
3365
+ const salt = fromHex(payload.SALT);
3366
+
3367
+ // ENCRYPTED_BLOB can be a number array (from EMET) or hex string
3368
+ let ciphertext;
3369
+ if (Array.isArray(payload.ENCRYPTED_BLOB)) {
3370
+ ciphertext = new Uint8Array(payload.ENCRYPTED_BLOB);
3371
+ } else {
3372
+ ciphertext = fromHex(payload.ENCRYPTED_BLOB);
3373
+ }
3374
+
3375
+ const coin = $('hd-coin')?.value || '0';
3376
+ const account = $('hd-account')?.value || '0';
3377
+ const index = $('hd-index')?.value || '0';
3378
+ const encPath = buildEncryptionPath(coin, account, index);
3379
+ const recipientKey = deriveHDKey(encPath);
3380
+ const recipientPriv = recipientKey.privateKey();
3381
+
3382
+ // 1. ECDH shared secret (using sender's public key)
3383
+ const shared = w.curves.secp256k1.ecdh(recipientPriv, senderPub);
3384
+
3385
+ // 2. HKDF: derive same AES key
3386
+ const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
3387
+ const aesKey = w.utils.hkdf(shared, salt, info, 32);
3388
+
3389
+ // 3. AES-256-GCM decrypt
3390
+ const decrypted = w.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv);
3391
+ const decStr = new TextDecoder().decode(decrypted);
3392
+
3393
+ $('decrypt-result-value').textContent = decStr;
3394
+ $('decrypt-result').style.display = 'block';
3395
+ } catch (err) {
3396
+ console.error('Decryption failed:', err);
3397
+ alert('Decryption failed: ' + err.message);
3398
+ }
3399
+ });
3400
+
3401
+ // Enable decrypt button when payload is pasted
3402
+ $('decrypt-payload')?.addEventListener('input', () => {
3403
+ const btn = $('decrypt-btn');
3404
+ if (btn) btn.disabled = !$('decrypt-payload')?.value?.trim();
3405
+ });
3406
+
3407
+ // Clear button
3408
+ $('encrypt-clear-btn')?.addEventListener('click', () => {
3409
+ $('encrypt-plaintext').value = '';
3410
+ $('encrypt-recipient-pubkey').value = '';
3411
+ $('encrypt-output-section').style.display = 'none';
3412
+ $('decrypt-payload').value = '';
3413
+ $('decrypt-result').style.display = 'none';
3414
+ $('encrypt-btn').disabled = !state.hdRoot;
3415
+ $('decrypt-btn').disabled = true;
3416
+ });
3132
3417
  }
3133
3418
 
3134
3419
  // =============================================================================
package/src/template.js CHANGED
@@ -4,13 +4,14 @@ export function getModalHTML() {
4
4
  <div id="keys-modal" class="modal">
5
5
  <div class="modal-glass modal-wide">
6
6
  <div class="modal-header"><div class="account-header-info"><div class="account-header-top"><h3>Account</h3><h3 class="account-total-value" id="account-total-value"></h3></div><div class="account-address-row"><select id="account-address-select" class="glass-select compact account-address-select"><option value="xpub">xpub</option></select><code class="account-address-display" id="account-address-display"></code><button class="account-address-copy" id="account-address-copy" title="Copy address"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><span class="account-address-value" id="account-address-value"></span></div></div><button class="modal-close">&times;</button></div>
7
+ <div class="modal-tabs">
8
+ <button class="modal-tab active" data-modal-tab="vcard-tab-content">Identity</button>
9
+ <button class="modal-tab" data-modal-tab="keys-tab-content">Keys</button>
10
+ <button class="modal-tab" data-modal-tab="trust-tab-content">Trust Map</button>
11
+ <button class="modal-tab" data-modal-tab="bond-tab-content">Security Bond</button>
12
+ <button class="modal-tab" data-modal-tab="encryption-tab-content">Encrypt</button>
13
+ </div>
7
14
  <div class="modal-body">
8
- <div class="modal-tabs">
9
- <button class="modal-tab active" data-modal-tab="vcard-tab-content">Identity</button>
10
- <button class="modal-tab" data-modal-tab="keys-tab-content">Keys</button>
11
- <button class="modal-tab" data-modal-tab="trust-tab-content">Trust Map</button>
12
- <button class="modal-tab" data-modal-tab="bond-tab-content">Security Bond</button>
13
- </div>
14
15
  <div id="keys-tab-content" class="modal-tab-content">
15
16
  <div id="memory-info-box" class="memory-info-box" style="display:none"><div class="memory-info-content"><p><strong>Your keys never leave your device.</strong></p></div><button class="wallet-info-close" id="memory-info-close" title="Close">&times;</button></div>
16
17
  <div class="xpub-section">
@@ -271,6 +272,94 @@ export function getModalHTML() {
271
272
  </div>
272
273
  </div>
273
274
 
275
+ <!-- Encryption Tab -->
276
+ <div id="encryption-tab-content" class="modal-tab-content">
277
+ <div class="encrypt-tab-intro">
278
+ <h4 class="section-label">ECIES Encrypt / Decrypt</h4>
279
+ <p>End-to-end encryption using your HD wallet keys. Uses ECDH key agreement + HKDF + AES-256-GCM (ECIES).</p>
280
+ </div>
281
+
282
+ <div class="encrypt-keys-section">
283
+ <div class="encrypt-key-row">
284
+ <div class="encrypt-key-card glass-card">
285
+ <div class="encrypt-key-header">
286
+ <span class="encrypt-role-badge sender">Sender (You)</span>
287
+ </div>
288
+ <div class="encrypt-key-detail">
289
+ <label>Encryption Public Key</label>
290
+ <code id="encrypt-sender-pubkey" class="truncate">--</code>
291
+ </div>
292
+ <div class="encrypt-key-detail">
293
+ <label>Derivation Path</label>
294
+ <code id="encrypt-sender-path">--</code>
295
+ </div>
296
+ </div>
297
+ <div class="encrypt-key-card glass-card">
298
+ <div class="encrypt-key-header">
299
+ <span class="encrypt-role-badge recipient">Recipient</span>
300
+ </div>
301
+ <div class="encrypt-key-detail">
302
+ <label>Recipient Public Key (hex)</label>
303
+ <div class="encrypt-recipient-input-row">
304
+ <input type="text" id="encrypt-recipient-pubkey" class="glass-input compact" placeholder="Paste recipient's secp256k1 public key (hex)">
305
+ <button id="encrypt-use-self" class="glass-btn small" title="Use your own key (for testing)">Self</button>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <div class="encrypt-message-section">
313
+ <div class="encrypt-input-group">
314
+ <label class="section-label">Message</label>
315
+ <textarea id="encrypt-plaintext" class="glass-input glass-textarea" rows="3" placeholder="Enter a message to encrypt..."></textarea>
316
+ </div>
317
+ <div class="encrypt-actions">
318
+ <button id="encrypt-btn" class="glass-btn primary" disabled>Encrypt</button>
319
+ <button id="decrypt-btn" class="glass-btn" disabled>Decrypt</button>
320
+ <button id="encrypt-clear-btn" class="glass-btn small">Clear</button>
321
+ </div>
322
+ </div>
323
+
324
+ <div id="encrypt-output-section" class="encrypt-output-section" style="display:none;">
325
+ <h4 class="section-label">Encrypted Payload</h4>
326
+ <div class="encrypt-output-fields">
327
+ <div class="encrypt-field"><label>Ciphertext</label><code id="encrypt-out-ciphertext" class="encrypt-out-value truncate"></code><button class="copy-btn" data-copy="encrypt-out-ciphertext" title="Copy"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button></div>
328
+ <div class="encrypt-field"><label>Auth Tag</label><code id="encrypt-out-tag" class="encrypt-out-value truncate"></code></div>
329
+ <div class="encrypt-field"><label>IV (nonce)</label><code id="encrypt-out-iv" class="encrypt-out-value truncate"></code></div>
330
+ <div class="encrypt-field"><label>HKDF Salt</label><code id="encrypt-out-salt" class="encrypt-out-value truncate"></code></div>
331
+ <div class="encrypt-field"><label>Sender Public Key</label><code id="encrypt-out-sender-pub" class="encrypt-out-value truncate"></code></div>
332
+ </div>
333
+ <div class="encrypt-bundle-group">
334
+ <div class="encrypt-format-toggle">
335
+ <label class="section-label">Payload Bundle</label>
336
+ <div class="encrypt-format-btns">
337
+ <button class="glass-btn small encrypt-fmt-btn active" data-format="json">JSON</button>
338
+ <button class="glass-btn small encrypt-fmt-btn" data-format="flatbuffer">FlatBuffer</button>
339
+ </div>
340
+ </div>
341
+ <div class="encrypt-format-info">
342
+ <span id="encrypt-format-label" class="encrypt-format-label">EME (Encrypted Message Envelope) — SpaceDataStandards.org</span>
343
+ </div>
344
+ <textarea id="encrypt-bundle" class="glass-input glass-textarea" rows="4" readonly></textarea>
345
+ <div class="encrypt-bundle-actions">
346
+ <button class="glass-btn small" id="encrypt-copy-bundle">Copy</button>
347
+ <button class="glass-btn small" id="encrypt-download-bundle">Download</button>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <div class="encrypt-decrypt-section">
353
+ <h4 class="section-label">Decrypt a Message</h4>
354
+ <p class="encrypt-decrypt-info">Paste an EME payload (JSON or base64 FlatBuffer) to decrypt with your key.</p>
355
+ <textarea id="decrypt-payload" class="glass-input glass-textarea" rows="4" placeholder='Paste EME JSON or base64 FlatBuffer here...'></textarea>
356
+ <div id="decrypt-result" class="decrypt-result" style="display:none;">
357
+ <label>Decrypted Message</label>
358
+ <div class="decrypt-result-value" id="decrypt-result-value"></div>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
274
363
  </div>
275
364
  </div>
276
365
  </div>
package/styles/main.css CHANGED
@@ -616,39 +616,42 @@ body:has(.modal.active) .nav-bar {
616
616
  display: block;
617
617
  }
618
618
 
619
- /* Modal Tabs (Account modal - Keys / vCard) */
619
+ /* Modal Tabs (Account modal - Keys / vCard) - Bootstrap-style nav tabs */
620
620
  .modal-tabs {
621
621
  display: flex;
622
- align-items: center;
623
- gap: 4px;
624
- margin-bottom: 20px;
625
- padding: 4px;
626
- background: var(--white-05);
627
- border-radius: var(--radius-sm);
622
+ gap: 0;
623
+ margin: 0;
624
+ padding: 0 24px;
625
+ background: transparent;
626
+ border-bottom: 2px solid var(--white-10, rgba(255,255,255,0.1));
628
627
  }
629
628
 
630
629
  .modal-tab {
631
- flex: 1;
632
- padding: 8px 4px;
630
+ padding: 10px 16px;
633
631
  background: transparent;
634
632
  border: none;
635
- border-radius: 10px;
633
+ border-bottom: 2px solid transparent;
634
+ margin-bottom: -2px;
636
635
  color: var(--white-60);
637
636
  font-family: var(--font-sans);
638
637
  font-size: 13px;
639
638
  font-weight: 500;
640
639
  line-height: 1;
641
- display: flex;
642
- align-items: center;
643
- justify-content: center;
644
640
  cursor: pointer;
645
- transition: all 0.2s;
641
+ transition: color 0.2s, border-color 0.2s;
646
642
  text-align: center;
643
+ white-space: nowrap;
644
+ }
645
+
646
+ .modal-tab:hover {
647
+ color: var(--white, #fff);
648
+ border-bottom-color: var(--white-30, rgba(255,255,255,0.3));
647
649
  }
648
650
 
649
651
  .modal-tab.active {
650
- background: var(--glass-hover);
651
- color: var(--white);
652
+ color: var(--white, #fff);
653
+ border-bottom-color: var(--accent, #00dc82);
654
+ background: transparent;
652
655
  }
653
656
 
654
657
  .modal-tab-content {
@@ -3991,46 +3994,35 @@ body:has(.modal.active) .nav-bar {
3991
3994
  min-width: 0;
3992
3995
  }
3993
3996
 
3994
- .modal-tabs {
3995
- flex-shrink: 0;
3996
- position: sticky;
3997
- top: 0;
3998
- z-index: 10;
3999
- background: var(--glass-bg);
4000
- backdrop-filter: var(--glass-blur);
4001
- border-bottom: 1px solid var(--glass-border);
4002
- margin: -24px -24px 0 -24px;
4003
- padding: 12px 24px;
4004
- }
4005
3997
 
4006
- .modal-tab-content {
3998
+ .modal-wide .modal-tab-content {
4007
3999
  flex: 1;
4008
4000
  overflow-y: auto;
4009
4001
  overflow-x: hidden;
4010
-
4002
+
4011
4003
  scrollbar-width: thin;
4012
4004
  scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
4013
4005
  }
4014
4006
 
4015
- .modal-tab-content::-webkit-scrollbar {
4007
+ .modal-wide .modal-tab-content::-webkit-scrollbar {
4016
4008
  width: 3px;
4017
4009
  }
4018
4010
 
4019
- .modal-tab-content::-webkit-scrollbar-track {
4011
+ .modal-wide .modal-tab-content::-webkit-scrollbar-track {
4020
4012
  background: transparent;
4021
4013
  }
4022
4014
 
4023
- .modal-tab-content::-webkit-scrollbar-thumb {
4015
+ .modal-wide .modal-tab-content::-webkit-scrollbar-thumb {
4024
4016
  background: rgba(255, 255, 255, 0.2);
4025
4017
  border-radius: 3px;
4026
4018
  }
4027
4019
 
4028
- .modal-tab-content::-webkit-scrollbar-thumb:hover {
4020
+ .modal-wide .modal-tab-content::-webkit-scrollbar-thumb:hover {
4029
4021
  background: rgba(255, 255, 255, 0.3);
4030
4022
  }
4031
4023
 
4032
4024
  /* Ensure content doesn't get cut off */
4033
- .modal-tab-content.active {
4025
+ .modal-wide .modal-tab-content.active {
4034
4026
  display: block;
4035
4027
  }
4036
4028
 
@@ -4142,6 +4134,7 @@ body:has(.modal.active) .nav-bar {
4142
4134
  padding: 80px 24px;
4143
4135
  position: relative;
4144
4136
  z-index: 1;
4137
+ overflow: hidden;
4145
4138
  }
4146
4139
 
4147
4140
  .page-section {
@@ -4163,6 +4156,7 @@ html {
4163
4156
  .section-container {
4164
4157
  max-width: 1100px;
4165
4158
  margin: 0 auto;
4159
+ width: 100%;
4166
4160
  }
4167
4161
 
4168
4162
  .section-container.section-sm {
@@ -4237,6 +4231,8 @@ html {
4237
4231
  border: 1px solid var(--glass-border);
4238
4232
  border-radius: var(--radius-sm);
4239
4233
  overflow: hidden;
4234
+ width: 100%;
4235
+ min-width: 0;
4240
4236
  }
4241
4237
 
4242
4238
  .code-block-header {
@@ -4277,6 +4273,8 @@ html {
4277
4273
  font-size: 0.8125rem;
4278
4274
  line-height: 1.7;
4279
4275
  color: var(--white-70);
4276
+ width: 100%;
4277
+ min-width: 0;
4280
4278
  }
4281
4279
 
4282
4280
  .chains-grid {
@@ -4288,6 +4286,17 @@ html {
4288
4286
  .chain-card {
4289
4287
  padding: 20px;
4290
4288
  text-align: center;
4289
+ display: flex;
4290
+ flex-direction: column;
4291
+ align-items: center;
4292
+ text-decoration: none;
4293
+ color: inherit;
4294
+ transition: transform 0.15s ease, border-color 0.15s ease;
4295
+ }
4296
+
4297
+ a.chain-card:hover {
4298
+ transform: translateY(-2px);
4299
+ border-color: var(--white-20);
4291
4300
  }
4292
4301
 
4293
4302
  .chain-card strong {
@@ -4519,3 +4528,208 @@ html {
4519
4528
  .example-code.active {
4520
4529
  display: block;
4521
4530
  }
4531
+
4532
+ @media (max-width: 600px) {
4533
+ .code-block pre {
4534
+ padding: 14px;
4535
+ font-size: 0.7rem;
4536
+ line-height: 1.6;
4537
+ }
4538
+ .example-tab {
4539
+ padding: 8px 8px;
4540
+ font-size: 0.75rem;
4541
+ }
4542
+ .section-sm {
4543
+ padding-left: 8px;
4544
+ padding-right: 8px;
4545
+ }
4546
+ }
4547
+
4548
+ /* =============================================================================
4549
+ Encryption Tab
4550
+ ============================================================================= */
4551
+
4552
+ .encrypt-tab-intro {
4553
+ margin-bottom: 20px;
4554
+ }
4555
+ .encrypt-tab-intro p {
4556
+ font-size: 0.85rem;
4557
+ color: var(--white-60);
4558
+ margin: 4px 0 0;
4559
+ }
4560
+
4561
+ .encrypt-key-row {
4562
+ display: grid;
4563
+ grid-template-columns: 1fr 1fr;
4564
+ gap: 12px;
4565
+ margin-bottom: 20px;
4566
+ }
4567
+ @media (max-width: 640px) {
4568
+ .encrypt-key-row {
4569
+ grid-template-columns: 1fr;
4570
+ }
4571
+ }
4572
+
4573
+ .encrypt-key-card {
4574
+ padding: 14px;
4575
+ border-radius: var(--radius-sm);
4576
+ }
4577
+ .encrypt-key-header {
4578
+ margin-bottom: 10px;
4579
+ }
4580
+ .encrypt-role-badge {
4581
+ font-size: 0.7rem;
4582
+ text-transform: uppercase;
4583
+ letter-spacing: 0.08em;
4584
+ font-weight: 600;
4585
+ padding: 3px 8px;
4586
+ border-radius: 6px;
4587
+ }
4588
+ .encrypt-role-badge.sender {
4589
+ background: rgba(59, 130, 246, 0.2);
4590
+ color: #60a5fa;
4591
+ }
4592
+ .encrypt-role-badge.recipient {
4593
+ background: rgba(16, 185, 129, 0.2);
4594
+ color: #34d399;
4595
+ }
4596
+ .encrypt-key-detail {
4597
+ margin-bottom: 8px;
4598
+ }
4599
+ .encrypt-key-detail label {
4600
+ display: block;
4601
+ font-size: 0.7rem;
4602
+ text-transform: uppercase;
4603
+ letter-spacing: 0.06em;
4604
+ color: var(--white-40);
4605
+ margin-bottom: 4px;
4606
+ }
4607
+ .encrypt-key-detail code {
4608
+ font-size: 0.8rem;
4609
+ color: var(--white-70);
4610
+ word-break: break-all;
4611
+ }
4612
+ .encrypt-recipient-input-row {
4613
+ display: flex;
4614
+ gap: 8px;
4615
+ align-items: center;
4616
+ }
4617
+ .encrypt-recipient-input-row input {
4618
+ flex: 1;
4619
+ min-width: 0;
4620
+ }
4621
+
4622
+ .encrypt-message-section {
4623
+ margin-bottom: 20px;
4624
+ }
4625
+ .encrypt-actions {
4626
+ display: flex;
4627
+ gap: 8px;
4628
+ margin-top: 10px;
4629
+ }
4630
+
4631
+ .encrypt-output-section {
4632
+ margin-bottom: 24px;
4633
+ padding: 16px;
4634
+ background: var(--glass-bg);
4635
+ border: 1px solid var(--glass-border);
4636
+ border-radius: var(--radius-sm);
4637
+ }
4638
+ .encrypt-output-fields {
4639
+ display: flex;
4640
+ flex-direction: column;
4641
+ gap: 8px;
4642
+ margin-bottom: 16px;
4643
+ }
4644
+ .encrypt-field {
4645
+ display: flex;
4646
+ align-items: center;
4647
+ gap: 8px;
4648
+ }
4649
+ .encrypt-field label {
4650
+ font-size: 0.7rem;
4651
+ text-transform: uppercase;
4652
+ letter-spacing: 0.06em;
4653
+ color: var(--white-40);
4654
+ white-space: nowrap;
4655
+ min-width: 100px;
4656
+ }
4657
+ .encrypt-out-value {
4658
+ font-size: 0.8rem;
4659
+ color: var(--white-70);
4660
+ overflow: hidden;
4661
+ text-overflow: ellipsis;
4662
+ white-space: nowrap;
4663
+ flex: 1;
4664
+ min-width: 0;
4665
+ }
4666
+ .encrypt-bundle-group {
4667
+ margin-top: 12px;
4668
+ }
4669
+ .encrypt-format-toggle {
4670
+ display: flex;
4671
+ align-items: center;
4672
+ justify-content: space-between;
4673
+ margin-bottom: 6px;
4674
+ }
4675
+ .encrypt-format-btns {
4676
+ display: flex;
4677
+ gap: 4px;
4678
+ }
4679
+ .encrypt-fmt-btn {
4680
+ font-size: 0.72rem !important;
4681
+ padding: 4px 10px !important;
4682
+ opacity: 0.5;
4683
+ transition: opacity 0.2s;
4684
+ }
4685
+ .encrypt-fmt-btn.active {
4686
+ opacity: 1;
4687
+ border-color: var(--accent, #00dc82);
4688
+ color: var(--accent, #00dc82);
4689
+ }
4690
+ .encrypt-format-info {
4691
+ margin-bottom: 8px;
4692
+ }
4693
+ .encrypt-format-label {
4694
+ font-size: 0.72rem;
4695
+ color: var(--white-40);
4696
+ font-style: italic;
4697
+ }
4698
+ .encrypt-bundle-group textarea {
4699
+ font-size: 0.78rem;
4700
+ font-family: 'SF Mono', monospace;
4701
+ }
4702
+ .encrypt-bundle-actions {
4703
+ display: flex;
4704
+ gap: 8px;
4705
+ margin-top: 8px;
4706
+ }
4707
+
4708
+ .encrypt-decrypt-section {
4709
+ margin-bottom: 16px;
4710
+ }
4711
+ .encrypt-decrypt-info {
4712
+ font-size: 0.85rem;
4713
+ color: var(--white-60);
4714
+ margin: 4px 0 10px;
4715
+ }
4716
+ .decrypt-result {
4717
+ margin-top: 12px;
4718
+ padding: 12px;
4719
+ background: rgba(16, 185, 129, 0.1);
4720
+ border: 1px solid rgba(16, 185, 129, 0.25);
4721
+ border-radius: var(--radius-sm);
4722
+ }
4723
+ .decrypt-result label {
4724
+ display: block;
4725
+ font-size: 0.7rem;
4726
+ text-transform: uppercase;
4727
+ letter-spacing: 0.06em;
4728
+ color: #34d399;
4729
+ margin-bottom: 6px;
4730
+ }
4731
+ .decrypt-result-value {
4732
+ font-size: 0.95rem;
4733
+ color: var(--white);
4734
+ word-break: break-word;
4735
+ }