diff --git a/lite/README.md b/lite/README.md
index 30a070b..c71b755 100644
--- a/lite/README.md
+++ b/lite/README.md
@@ -205,22 +205,44 @@ The following features are planned but not yet implemented:
## Development
-To work on the source files:
+โ ๏ธ **CRITICAL: DO NOT EDIT `nostr-lite.js` DIRECTLY!**
+
+The `nostr-lite.js` file is **auto-generated** by the build script. All changes must be made in the build script itself.
+
+### Build Process
```bash
-# Edit individual components
-lite/core/nip46-client.js
-lite/ui/modal.js
-lite/nostr-login-lite.js
+# The main library source code is in:
+lite/build.js # โ Edit this file for library changes
-# Run bundler to create distribution
-node lite/bundler.js
+# To make changes:
+1. Edit lite/build.js # Contains all source code
+2. cd lite && node build.js # Regenerates nostr-lite.js
+3. Test your changes in examples/
-# Start dev server (from project root)
+# NEVER edit these files directly (they get overwritten):
+lite/nostr-lite.js # โ Auto-generated, don't edit!
+
+# Separate components that can be edited:
+lite/ui/modal.js # Modal UI component
+themes/default/theme.css # Default theme
+themes/dark/theme.css # Dark theme
+```
+
+### Development Workflow
+
+```bash
+# 1. Make changes to source
+nano lite/build.js
+
+# 2. Rebuild bundle
+cd lite && node build.js
+
+# 3. Start dev server (from project root)
python3 -m http.server 8000
-# Open test page
-open http://localhost:8000/examples/simple-demo.html
+# 4. Test changes
+open http://localhost:8000/examples/modal.html
```
### Local Bundle Setup
diff --git a/lite/build.js b/lite/build.js
index 5eb186d..92df2ac 100644
--- a/lite/build.js
+++ b/lite/build.js
@@ -1,9 +1,21 @@
/**
- * Simple script to create NOSTR_LOGIN_LITE bundle
- * For the new two-file architecture:
+ * ๐๏ธ NOSTR_LOGIN_LITE Build Script
+ *
+ * โ ๏ธ IMPORTANT: This file contains the source code for the NOSTR_LOGIN_LITE library!
+ * โ ๏ธ DO NOT edit lite/nostr-lite.js directly - it's auto-generated by this script!
+ * โ ๏ธ To modify the library, edit this file (build.js) and run: node build.js
+ *
+ * This script builds the two-file architecture:
* 1. nostr.bundle.js (official nostr-tools bundle - static file)
- * 2. nip46-extension.js (NIP-46 extension - static file)
- * 3. nostr-lite.js (NOSTR_LOGIN_LITE library - built by this script)
+ * 2. nostr-lite.js (NOSTR_LOGIN_LITE library - built by this script)
+ *
+ * Features included:
+ * - CSS-Only Theme System (no JSON duplication)
+ * - Modal UI Component
+ * - FloatingTab Component
+ * - Extension Bridge
+ * - Window.nostr facade
+ * - Main NostrLite class with all functionality
*/
const fs = require('fs');
@@ -26,10 +38,14 @@ function createNostrLoginLiteBundle() {
// Start with the bundle header
let bundle = `/**
* NOSTR_LOGIN_LITE - Authentication Library
+ *
+ * โ ๏ธ WARNING: THIS FILE IS AUTO-GENERATED - DO NOT EDIT MANUALLY!
+ * โ ๏ธ To make changes, edit lite/build.js and run: cd lite && node build.js
+ * โ ๏ธ Any manual edits to this file will be OVERWRITTEN when build.js runs!
+ *
* Two-file architecture:
* 1. Load nostr.bundle.js (official nostr-tools bundle)
- * 2. Load nip46-extension.js (extends NostrTools with NIP-46)
- * 3. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library)
+ * 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
* Generated on: ${new Date().toISOString()}
*/
@@ -40,22 +56,573 @@ if (typeof window !== 'undefined') {
throw new Error('Missing dependency: nostr.bundle.js');
}
- if (!window.NostrTools.nip46) {
- console.error('NOSTR_LOGIN_LITE: nip46-extension.js must be loaded after nostr.bundle.js');
- throw new Error('Missing dependency: nip46-extension.js');
- }
-
console.log('NOSTR_LOGIN_LITE: Dependencies verified โ');
console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools));
console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46);
}
+// ===== NIP-46 Extension Integration =====
+// Add NIP-46 functionality to NostrTools if not already present
+if (typeof window.NostrTools !== 'undefined' && !window.NostrTools.nip46) {
+ console.log('NOSTR_LOGIN_LITE: Adding NIP-46 extension to NostrTools');
+
+ const { nip44, generateSecretKey, getPublicKey, finalizeEvent, verifyEvent, utils } = window.NostrTools;
+
+ // NIP-05 regex for parsing
+ const NIP05_REGEX = /^(?:([\\w.+-]+)@)?([\\w_-]+(\.[\\w_-]+)+)$/;
+ const BUNKER_REGEX = /^bunker:\\/\\/([0-9a-f]{64})\\??([?\\/\\w:.=&%-]*)$/;
+ const EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
+
+ // Event kinds
+ const NostrConnect = 24133;
+ const ClientAuth = 22242;
+ const Handlerinformation = 31990;
+
+ // Fetch implementation
+ let _fetch;
+ try {
+ _fetch = fetch;
+ } catch {
+ _fetch = null;
+ }
+
+ function useFetchImplementation(fetchImplementation) {
+ _fetch = fetchImplementation;
+ }
+
+ // Simple Pool implementation for NIP-46
+ class SimplePool {
+ constructor() {
+ this.relays = new Map();
+ this.subscriptions = new Map();
+ }
+
+ async ensureRelay(url) {
+ if (!this.relays.has(url)) {
+ console.log(\`NIP-46: Connecting to relay \${url}\`);
+ const ws = new WebSocket(url);
+ const relay = {
+ ws,
+ connected: false,
+ subscriptions: new Map()
+ };
+
+ this.relays.set(url, relay);
+
+ // Wait for connection with proper event handlers
+ await new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ console.error(\`NIP-46: Connection timeout for \${url}\`);
+ reject(new Error(\`Connection timeout to \${url}\`));
+ }, 10000); // 10 second timeout
+
+ ws.onopen = () => {
+ console.log(\`NIP-46: Successfully connected to relay \${url}, WebSocket state: \${ws.readyState}\`);
+ relay.connected = true;
+ clearTimeout(timeout);
+ resolve();
+ };
+
+ ws.onerror = (error) => {
+ console.error(\`NIP-46: Failed to connect to \${url}:\`, error);
+ clearTimeout(timeout);
+ reject(new Error(\`Failed to connect to \${url}: \${error.message || 'Connection failed'}\`));
+ };
+
+ ws.onclose = (event) => {
+ console.log(\`NIP-46: Disconnected from relay \${url}:\`, event.code, event.reason);
+ relay.connected = false;
+ if (this.relays.has(url)) {
+ this.relays.delete(url);
+ }
+ clearTimeout(timeout);
+ reject(new Error(\`Connection closed during setup: \${event.reason || 'Unknown reason'}\`));
+ };
+ });
+ } else {
+ const relay = this.relays.get(url);
+ // Verify the existing connection is still open
+ if (!relay.connected || relay.ws.readyState !== WebSocket.OPEN) {
+ console.log(\`NIP-46: Reconnecting to relay \${url}\`);
+ this.relays.delete(url);
+ return await this.ensureRelay(url); // Recursively reconnect
+ }
+ }
+
+ const relay = this.relays.get(url);
+ console.log(\`NIP-46: Relay \${url} ready, WebSocket state: \${relay.ws.readyState}\`);
+ return relay;
+ }
+
+ subscribe(relays, filters, params = {}) {
+ const subId = Math.random().toString(36).substring(7);
+
+ relays.forEach(async (url) => {
+ try {
+ const relay = await this.ensureRelay(url);
+
+ relay.ws.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data[0] === 'EVENT' && data[1] === subId) {
+ params.onevent?.(data[2]);
+ } else if (data[0] === 'EOSE' && data[1] === subId) {
+ params.oneose?.();
+ }
+ } catch (err) {
+ console.warn('Failed to parse message:', err);
+ }
+ };
+
+ // Ensure filters is an array
+ const filtersArray = Array.isArray(filters) ? filters : [filters];
+ const reqMsg = JSON.stringify(['REQ', subId, ...filtersArray]);
+ relay.ws.send(reqMsg);
+
+ } catch (err) {
+ console.warn('Failed to connect to relay:', url, err);
+ }
+ });
+
+ return {
+ close: () => {
+ relays.forEach(async (url) => {
+ const relay = this.relays.get(url);
+ if (relay?.connected) {
+ relay.ws.send(JSON.stringify(['CLOSE', subId]));
+ }
+ });
+ }
+ };
+ }
+
+ async publish(relays, event) {
+ console.log(\`NIP-46: Publishing event to \${relays.length} relays:\`, event);
+
+ const promises = relays.map(async (url) => {
+ try {
+ console.log(\`NIP-46: Attempting to publish to \${url}\`);
+ const relay = await this.ensureRelay(url);
+
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ console.error(\`NIP-46: Publish timeout to \${url}\`);
+ reject(new Error(\`Publish timeout to \${url}\`));
+ }, 10000); // Increased timeout to 10 seconds
+
+ // Set up message handler for this specific event
+ const messageHandler = (msg) => {
+ try {
+ const data = JSON.parse(msg.data);
+ if (data[0] === 'OK' && data[1] === event.id) {
+ clearTimeout(timeout);
+ relay.ws.removeEventListener('message', messageHandler);
+ if (data[2]) {
+ console.log(\`NIP-46: Publish success to \${url}:\`, data[3]);
+ resolve(data[3]);
+ } else {
+ console.error(\`NIP-46: Publish rejected by \${url}:\`, data[3]);
+ reject(new Error(\`Publish rejected: \${data[3]}\`));
+ }
+ }
+ } catch (err) {
+ console.error(\`NIP-46: Error parsing message from \${url}:\`, err);
+ clearTimeout(timeout);
+ relay.ws.removeEventListener('message', messageHandler);
+ reject(err);
+ }
+ };
+
+ relay.ws.addEventListener('message', messageHandler);
+
+ // Double-check WebSocket state before sending
+ console.log(\`NIP-46: About to publish to \${url}, WebSocket state: \${relay.ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)\`);
+ if (relay.ws.readyState === WebSocket.OPEN) {
+ console.log(\`NIP-46: Sending event to \${url}\`);
+ relay.ws.send(JSON.stringify(['EVENT', event]));
+ } else {
+ console.error(\`NIP-46: WebSocket not ready for \${url}, state: \${relay.ws.readyState}\`);
+ clearTimeout(timeout);
+ relay.ws.removeEventListener('message', messageHandler);
+ reject(new Error(\`WebSocket not ready for \${url}, state: \${relay.ws.readyState}\`));
+ }
+ });
+ } catch (err) {
+ console.error(\`NIP-46: Failed to publish to \${url}:\`, err);
+ return Promise.reject(new Error(\`Failed to publish to \${url}: \${err.message}\`));
+ }
+ });
+
+ const results = await Promise.allSettled(promises);
+ console.log(\`NIP-46: Publish results:\`, results);
+ return results;
+ }
+
+ async querySync(relays, filter, params = {}) {
+ return new Promise((resolve) => {
+ const events = [];
+ this.subscribe(relays, [filter], {
+ ...params,
+ onevent: (event) => events.push(event),
+ oneose: () => resolve(events)
+ });
+ });
+ }
+ }
+
+ // Bunker URL utilities
+ function toBunkerURL(bunkerPointer) {
+ let bunkerURL = new URL(\`bunker://\${bunkerPointer.pubkey}\`);
+ bunkerPointer.relays.forEach((relay) => {
+ bunkerURL.searchParams.append('relay', relay);
+ });
+ if (bunkerPointer.secret) {
+ bunkerURL.searchParams.set('secret', bunkerPointer.secret);
+ }
+ return bunkerURL.toString();
+ }
+
+ async function parseBunkerInput(input) {
+ let match = input.match(BUNKER_REGEX);
+ if (match) {
+ try {
+ const pubkey = match[1];
+ const qs = new URLSearchParams(match[2]);
+ return {
+ pubkey,
+ relays: qs.getAll('relay'),
+ secret: qs.get('secret')
+ };
+ } catch (_err) {
+ // Continue to NIP-05 parsing
+ }
+ }
+ return queryBunkerProfile(input);
+ }
+
+ async function queryBunkerProfile(nip05) {
+ if (!_fetch) {
+ throw new Error('Fetch implementation not available');
+ }
+
+ const match = nip05.match(NIP05_REGEX);
+ if (!match) return null;
+
+ const [_, name = '_', domain] = match;
+ try {
+ const url = \`https://\${domain}/.well-known/nostr.json?name=\${name}\`;
+ const res = await (await _fetch(url, { redirect: 'error' })).json();
+ let pubkey = res.names[name];
+ let relays = res.nip46[pubkey] || [];
+ return { pubkey, relays, secret: null };
+ } catch (_err) {
+ return null;
+ }
+ }
+
+ // BunkerSigner class
+ class BunkerSigner {
+ constructor(clientSecretKey, bp, params = {}) {
+ if (bp.relays.length === 0) {
+ throw new Error('no relays are specified for this bunker');
+ }
+
+ this.params = params;
+ this.pool = params.pool || new SimplePool();
+ this.secretKey = clientSecretKey;
+ this.conversationKey = nip44.getConversationKey(clientSecretKey, bp.pubkey);
+ this.bp = bp;
+ this.isOpen = false;
+ this.idPrefix = Math.random().toString(36).substring(7);
+ this.serial = 0;
+ this.listeners = {};
+ this.waitingForAuth = {};
+ this.ready = false;
+ this.readyPromise = this.setupSubscription(params);
+ }
+
+ async setupSubscription(params) {
+ console.log('NIP-46: Setting up subscription to relays:', this.bp.relays);
+ const listeners = this.listeners;
+ const waitingForAuth = this.waitingForAuth;
+ const convKey = this.conversationKey;
+
+ // Ensure all relays are connected first
+ await Promise.all(this.bp.relays.map(url => this.pool.ensureRelay(url)));
+ console.log('NIP-46: All relays connected, setting up subscription');
+
+ this.subCloser = this.pool.subscribe(
+ this.bp.relays,
+ [{ kinds: [NostrConnect], authors: [this.bp.pubkey], '#p': [getPublicKey(this.secretKey)] }],
+ {
+ onevent: async (event) => {
+ const o = JSON.parse(nip44.decrypt(event.content, convKey));
+ const { id, result, error } = o;
+
+ if (result === 'auth_url' && waitingForAuth[id]) {
+ delete waitingForAuth[id];
+ if (params.onauth) {
+ params.onauth(error);
+ } else {
+ console.warn(
+ \`NIP-46: remote signer \${this.bp.pubkey} tried to send an "auth_url"='\${error}' but there was no onauth() callback configured.\`
+ );
+ }
+ return;
+ }
+
+ let handler = listeners[id];
+ if (handler) {
+ if (error) handler.reject(error);
+ else if (result) handler.resolve(result);
+ delete listeners[id];
+ }
+ },
+ onclose: () => {
+ this.subCloser = undefined;
+ }
+ }
+ );
+
+ this.isOpen = true;
+ this.ready = true;
+ console.log('NIP-46: BunkerSigner setup complete and ready');
+ }
+
+ async ensureReady() {
+ if (!this.ready) {
+ console.log('NIP-46: Waiting for BunkerSigner to be ready...');
+ await this.readyPromise;
+ }
+ }
+
+ async close() {
+ this.isOpen = false;
+ this.subCloser?.close();
+ }
+
+ async sendRequest(method, params) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await this.ensureReady(); // Wait for BunkerSigner to be ready
+
+ if (!this.isOpen) {
+ throw new Error('this signer is not open anymore, create a new one');
+ }
+ if (!this.subCloser) {
+ await this.setupSubscription(this.params);
+ }
+
+ this.serial++;
+ const id = \`\${this.idPrefix}-\${this.serial}\`;
+ const encryptedContent = nip44.encrypt(JSON.stringify({ id, method, params }), this.conversationKey);
+
+ const verifiedEvent = finalizeEvent(
+ {
+ kind: NostrConnect,
+ tags: [['p', this.bp.pubkey]],
+ content: encryptedContent,
+ created_at: Math.floor(Date.now() / 1000)
+ },
+ this.secretKey
+ );
+
+ this.listeners[id] = { resolve, reject };
+ this.waitingForAuth[id] = true;
+
+ console.log(\`NIP-46: Sending \${method} request with id \${id}\`);
+ const publishResults = await this.pool.publish(this.bp.relays, verifiedEvent);
+ // Check if at least one publish succeeded
+ const hasSuccess = publishResults.some(result => result.status === 'fulfilled');
+ if (!hasSuccess) {
+ throw new Error('Failed to publish to any relay');
+ }
+ console.log(\`NIP-46: \${method} request sent successfully\`);
+ } catch (err) {
+ console.error(\`NIP-46: sendRequest \${method} failed:\`, err);
+ reject(err);
+ }
+ });
+ }
+
+ async ping() {
+ let resp = await this.sendRequest('ping', []);
+ if (resp !== 'pong') {
+ throw new Error(\`result is not pong: \${resp}\`);
+ }
+ }
+
+ async connect() {
+ await this.sendRequest('connect', [this.bp.pubkey, this.bp.secret || '']);
+ }
+
+ async getPublicKey() {
+ if (!this.cachedPubKey) {
+ this.cachedPubKey = await this.sendRequest('get_public_key', []);
+ }
+ return this.cachedPubKey;
+ }
+
+ async signEvent(event) {
+ let resp = await this.sendRequest('sign_event', [JSON.stringify(event)]);
+ let signed = JSON.parse(resp);
+ if (verifyEvent(signed)) {
+ return signed;
+ } else {
+ throw new Error(\`event returned from bunker is improperly signed: \${JSON.stringify(signed)}\`);
+ }
+ }
+
+ async nip04Encrypt(thirdPartyPubkey, plaintext) {
+ return await this.sendRequest('nip04_encrypt', [thirdPartyPubkey, plaintext]);
+ }
+
+ async nip04Decrypt(thirdPartyPubkey, ciphertext) {
+ return await this.sendRequest('nip04_decrypt', [thirdPartyPubkey, ciphertext]);
+ }
+
+ async nip44Encrypt(thirdPartyPubkey, plaintext) {
+ return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, plaintext]);
+ }
+
+ async nip44Decrypt(thirdPartyPubkey, ciphertext) {
+ return await this.sendRequest('nip44_decrypt', [thirdPartyPubkey, ciphertext]);
+ }
+ }
+
+ async function createAccount(bunker, params, username, domain, email, localSecretKey = generateSecretKey()) {
+ if (email && !EMAIL_REGEX.test(email)) {
+ throw new Error('Invalid email');
+ }
+
+ let rpc = new BunkerSigner(localSecretKey, bunker.bunkerPointer, params);
+ let pubkey = await rpc.sendRequest('create_account', [username, domain, email || '']);
+ rpc.bp.pubkey = pubkey;
+ await rpc.connect();
+ return rpc;
+ }
+
+ async function fetchBunkerProviders(pool, relays) {
+ const events = await pool.querySync(relays, {
+ kinds: [Handlerinformation],
+ '#k': [NostrConnect.toString()]
+ });
+
+ events.sort((a, b) => b.created_at - a.created_at);
+
+ const validatedBunkers = await Promise.all(
+ events.map(async (event, i) => {
+ try {
+ const content = JSON.parse(event.content);
+ try {
+ if (events.findIndex((ev) => JSON.parse(ev.content).nip05 === content.nip05) !== i) {
+ return undefined;
+ }
+ } catch (err) {
+ // Continue processing
+ }
+
+ const bp = await queryBunkerProfile(content.nip05);
+ if (bp && bp.pubkey === event.pubkey && bp.relays.length) {
+ return {
+ bunkerPointer: bp,
+ nip05: content.nip05,
+ domain: content.nip05.split('@')[1],
+ name: content.name || content.display_name,
+ picture: content.picture,
+ about: content.about,
+ website: content.website,
+ local: false
+ };
+ }
+ } catch (err) {
+ return undefined;
+ }
+ })
+ );
+
+ return validatedBunkers.filter((b) => b !== undefined);
+ }
+
+ // Extend NostrTools with NIP-46 functionality
+ window.NostrTools.nip46 = {
+ BunkerSigner,
+ parseBunkerInput,
+ toBunkerURL,
+ queryBunkerProfile,
+ createAccount,
+ fetchBunkerProviders,
+ useFetchImplementation,
+ BUNKER_REGEX,
+ SimplePool
+ };
+
+ console.log('NIP-46 extension loaded successfully');
+ console.log('Available: NostrTools.nip46');
+}
+
// ======================================
// NOSTR_LOGIN_LITE Components
// ======================================
`;
+ // Embed CSS themes
+ console.log('๐จ Adding CSS-Only Theme System...');
+
+ const defaultThemeCssPath = path.join(__dirname, '../themes/default/theme.css');
+ const darkThemeCssPath = path.join(__dirname, '../themes/dark/theme.css');
+
+ if (fs.existsSync(defaultThemeCssPath) && fs.existsSync(darkThemeCssPath)) {
+ const defaultThemeCss = fs.readFileSync(defaultThemeCssPath, 'utf8')
+ .replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\${/g, '\\${');
+
+ const darkThemeCss = fs.readFileSync(darkThemeCssPath, 'utf8')
+ .replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\${/g, '\\${');
+
+ bundle += `// ======================================\n`;
+ bundle += `// CSS-Only Theme System\n`;
+ bundle += `// ======================================\n\n`;
+
+ bundle += `const THEME_CSS = {\n`;
+ bundle += ` 'default': \`${defaultThemeCss}\`,\n`;
+ bundle += ` 'dark': \`${darkThemeCss}\`\n`;
+ bundle += `};\n\n`;
+
+ bundle += `// Theme management functions\n`;
+ bundle += `function injectThemeCSS(themeName = 'default') {\n`;
+ bundle += ` if (typeof document !== 'undefined') {\n`;
+ bundle += ` // Remove existing theme CSS\n`;
+ bundle += ` const existingStyle = document.getElementById('nl-theme-css');\n`;
+ bundle += ` if (existingStyle) {\n`;
+ bundle += ` existingStyle.remove();\n`;
+ bundle += ` }\n`;
+ bundle += ` \n`;
+ bundle += ` // Inject selected theme CSS\n`;
+ bundle += ` const themeCss = THEME_CSS[themeName] || THEME_CSS['default'];\n`;
+ bundle += ` const style = document.createElement('style');\n`;
+ bundle += ` style.id = 'nl-theme-css';\n`;
+ bundle += ` style.textContent = themeCss;\n`;
+ bundle += ` document.head.appendChild(style);\n`;
+ bundle += ` console.log(\`NOSTR_LOGIN_LITE: \${themeName} theme CSS injected\`);\n`;
+ bundle += ` }\n`;
+ bundle += `}\n\n`;
+
+ // Auto-inject default theme on load
+ bundle += `// Auto-inject default theme when DOM is ready\n`;
+ bundle += `if (typeof document !== 'undefined') {\n`;
+ bundle += ` if (document.readyState === 'loading') {\n`;
+ bundle += ` document.addEventListener('DOMContentLoaded', () => injectThemeCSS('default'));\n`;
+ bundle += ` } else {\n`;
+ bundle += ` injectThemeCSS('default');\n`;
+ bundle += ` }\n`;
+ bundle += `}\n\n`;
+ }
+
// Add Modal UI
const modalPath = path.join(__dirname, 'ui/modal.js');
if (fs.existsSync(modalPath)) {
@@ -93,6 +660,338 @@ if (typeof window !== 'undefined') {
// Add main library code
console.log('๐ Adding Main Library...');
bundle += `
+// ======================================
+// FloatingTab Component (Recovered from git history)
+// ======================================
+
+class FloatingTab {
+ constructor(modal, options = {}) {
+ this.modal = modal;
+ this.options = {
+ enabled: true,
+ hPosition: 1.0, // 0.0 = left, 1.0 = right
+ vPosition: 0.5, // 0.0 = top, 1.0 = bottom
+ offset: { x: 0, y: 0 },
+ appearance: {
+ style: 'pill', // 'pill', 'square', 'circle'
+ theme: 'auto', // 'auto', 'light', 'dark'
+ icon: '[LOGIN]',
+ text: 'Login',
+ iconOnly: false
+ },
+ behavior: {
+ hideWhenAuthenticated: true,
+ showUserInfo: true,
+ autoSlide: true,
+ persistent: false
+ },
+ ...options
+ };
+
+ this.isAuthenticated = false;
+ this.userInfo = null;
+ this.container = null;
+ this.isVisible = false;
+
+ if (this.options.enabled) {
+ this._init();
+ }
+ }
+
+ _init() {
+ console.log('FloatingTab: Initializing with options:', this.options);
+ this._createContainer();
+ this._setupEventListeners();
+ this._updateAppearance();
+ this._position();
+ this.show();
+ }
+
+ _createContainer() {
+ // Remove existing floating tab if any
+ const existingTab = document.getElementById('nl-floating-tab');
+ if (existingTab) {
+ existingTab.remove();
+ }
+
+ this.container = document.createElement('div');
+ this.container.id = 'nl-floating-tab';
+ this.container.className = 'nl-floating-tab';
+
+ // Base styles - positioning and behavior
+ this.container.style.cssText = \`
+ position: fixed;
+ z-index: 9999;
+ cursor: pointer;
+ user-select: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ font-size: 14px;
+ font-weight: 500;
+ padding: 8px 16px;
+ min-width: 80px;
+ max-width: 200px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ \`;
+
+ document.body.appendChild(this.container);
+ }
+
+ _setupEventListeners() {
+ if (!this.container) return;
+
+ // Click handler
+ this.container.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this._handleClick();
+ });
+
+ // Hover effects
+ this.container.addEventListener('mouseenter', () => {
+ if (this.options.behavior.autoSlide) {
+ this._slideIn();
+ }
+ });
+
+ this.container.addEventListener('mouseleave', () => {
+ if (this.options.behavior.autoSlide) {
+ this._slideOut();
+ }
+ });
+
+ // Listen for authentication events
+ window.addEventListener('nlMethodSelected', (e) => {
+ console.log('FloatingTab: Authentication method selected:', e.detail);
+ this._handleAuth(e.detail);
+ });
+
+ window.addEventListener('nlLogout', () => {
+ console.log('FloatingTab: Logout detected');
+ this._handleLogout();
+ });
+ }
+
+ _handleClick() {
+ console.log('FloatingTab: Clicked');
+
+ if (this.isAuthenticated && this.options.behavior.showUserInfo) {
+ // Show user menu or profile options
+ this._showUserMenu();
+ } else {
+ // Open login modal
+ if (this.modal) {
+ this.modal.open({ startScreen: 'login' });
+ }
+ }
+ }
+
+ _handleAuth(authData) {
+ console.log('FloatingTab: Handling authentication:', authData);
+ this.isAuthenticated = true;
+ this.userInfo = authData;
+
+ if (this.options.behavior.hideWhenAuthenticated) {
+ this.hide();
+ } else {
+ this._updateAppearance();
+ }
+ }
+
+ _handleLogout() {
+ console.log('FloatingTab: Handling logout');
+ this.isAuthenticated = false;
+ this.userInfo = null;
+
+ if (this.options.behavior.hideWhenAuthenticated) {
+ this.show();
+ }
+
+ this._updateAppearance();
+ }
+
+ _showUserMenu() {
+ // Simple user menu - could be expanded
+ const menu = document.createElement('div');
+ menu.style.cssText = \`
+ position: fixed;
+ background: var(--nl-secondary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ padding: 12px;
+ z-index: 10000;
+ font-family: var(--nl-font-family);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ \`;
+
+ // Position near the floating tab
+ const tabRect = this.container.getBoundingClientRect();
+ if (this.options.hPosition > 0.5) {
+ // Tab is on right side, show menu to the left
+ menu.style.right = (window.innerWidth - tabRect.left) + 'px';
+ } else {
+ // Tab is on left side, show menu to the right
+ menu.style.left = tabRect.right + 'px';
+ }
+ menu.style.top = tabRect.top + 'px';
+
+ // Menu content
+ const userDisplay = this.userInfo?.pubkey ?
+ \`\${this.userInfo.pubkey.slice(0, 8)}...\${this.userInfo.pubkey.slice(-4)}\` :
+ 'Authenticated';
+
+ menu.innerHTML = \`
+
\${userDisplay}
+
+ \`;
+
+ document.body.appendChild(menu);
+
+ // Auto-remove menu after delay or on outside click
+ const removeMenu = () => menu.remove();
+ setTimeout(removeMenu, 5000);
+
+ document.addEventListener('click', function onOutsideClick(e) {
+ if (!menu.contains(e.target) && e.target !== this.container) {
+ removeMenu();
+ document.removeEventListener('click', onOutsideClick);
+ }
+ });
+ }
+
+ _updateAppearance() {
+ if (!this.container) return;
+
+ // Update content
+ if (this.isAuthenticated && this.options.behavior.showUserInfo) {
+ const display = this.userInfo?.pubkey ?
+ (this.options.appearance.iconOnly ?
+ '[USER]' :
+ \`[USER] \${this.userInfo.pubkey.slice(0, 6)}...\`) :
+ (this.options.appearance.iconOnly ? '[AUTH]' : '[AUTH] Logged In');
+
+ this.container.textContent = display;
+ this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
+ } else {
+ const display = this.options.appearance.iconOnly ?
+ this.options.appearance.icon :
+ \`\${this.options.appearance.icon} \${this.options.appearance.text}\`;
+
+ this.container.textContent = display;
+ this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
+ }
+
+ // Apply appearance styles based on current state
+ this._applyThemeStyles();
+ }
+
+ _applyThemeStyles() {
+ if (!this.container) return;
+
+ // The CSS classes will handle the theming through CSS custom properties
+ // Additional style customizations can be added here if needed
+
+ // Apply style variant
+ if (this.options.appearance.style === 'circle') {
+ this.container.style.borderRadius = '50%';
+ this.container.style.width = '48px';
+ this.container.style.height = '48px';
+ this.container.style.minWidth = '48px';
+ this.container.style.padding = '0';
+ } else if (this.options.appearance.style === 'square') {
+ this.container.style.borderRadius = '4px';
+ } else {
+ // pill style (default)
+ this.container.style.borderRadius = 'var(--nl-border-radius)';
+ }
+ }
+
+ _position() {
+ if (!this.container) return;
+
+ const padding = 16; // Distance from screen edge
+
+ // Calculate position based on percentage
+ const x = this.options.hPosition * (window.innerWidth - this.container.offsetWidth - padding * 2) + padding + this.options.offset.x;
+ const y = this.options.vPosition * (window.innerHeight - this.container.offsetHeight - padding * 2) + padding + this.options.offset.y;
+
+ this.container.style.left = \`\${x}px\`;
+ this.container.style.top = \`\${y}px\`;
+
+ console.log(\`FloatingTab: Positioned at (\${x}, \${y})\`);
+ }
+
+ _slideIn() {
+ if (!this.container || !this.options.behavior.autoSlide) return;
+
+ // Slide towards center slightly
+ const currentTransform = this.container.style.transform || '';
+ if (this.options.hPosition > 0.5) {
+ this.container.style.transform = currentTransform + ' translateX(-8px)';
+ } else {
+ this.container.style.transform = currentTransform + ' translateX(8px)';
+ }
+ }
+
+ _slideOut() {
+ if (!this.container || !this.options.behavior.autoSlide) return;
+
+ // Reset position
+ this.container.style.transform = '';
+ }
+
+ show() {
+ if (!this.container) return;
+ this.container.style.display = 'flex';
+ this.isVisible = true;
+ console.log('FloatingTab: Shown');
+ }
+
+ hide() {
+ if (!this.container) return;
+ this.container.style.display = 'none';
+ this.isVisible = false;
+ console.log('FloatingTab: Hidden');
+ }
+
+ destroy() {
+ if (this.container) {
+ this.container.remove();
+ this.container = null;
+ }
+ this.isVisible = false;
+ console.log('FloatingTab: Destroyed');
+ }
+
+ // Update options and re-apply
+ updateOptions(newOptions) {
+ this.options = { ...this.options, ...newOptions };
+ if (this.container) {
+ this._updateAppearance();
+ this._position();
+ }
+ }
+
+ // Get current state
+ getState() {
+ return {
+ isVisible: this.isVisible,
+ isAuthenticated: this.isAuthenticated,
+ userInfo: this.userInfo,
+ options: this.options
+ };
+ }
+}
+
// ======================================
// Main NOSTR_LOGIN_LITE Library
// ======================================
@@ -151,14 +1050,16 @@ class NostrLite {
this.options = {};
this.extensionBridge = new ExtensionBridge();
this.initialized = false;
+ this.currentTheme = 'default';
+ this.modal = null;
+ this.floatingTab = null;
}
async init(options = {}) {
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
this.options = {
- theme: 'light',
- darkMode: false,
+ theme: 'default',
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
methods: {
extension: true,
@@ -167,14 +1068,46 @@ class NostrLite {
connect: false,
otp: false
},
+ floatingTab: {
+ enabled: false,
+ hPosition: 1.0,
+ vPosition: 0.5,
+ offset: { x: 0, y: 0 },
+ appearance: {
+ style: 'pill',
+ theme: 'auto',
+ icon: '[LOGIN]',
+ text: 'Login',
+ iconOnly: false
+ },
+ behavior: {
+ hideWhenAuthenticated: true,
+ showUserInfo: true,
+ autoSlide: true,
+ persistent: false
+ }
+ },
...options
};
+ // Apply the selected theme (CSS-only)
+ this.switchTheme(this.options.theme);
+
// Set up window.nostr facade if no extension detected
if (this.extensionBridge.getExtensionCount() === 0) {
this._setupWindowNostrFacade();
}
+ // Create modal during init (matching original git architecture)
+ this.modal = new Modal(this.options);
+ console.log('NOSTR_LOGIN_LITE: Modal created during init');
+
+ // Initialize floating tab if enabled
+ if (this.options.floatingTab.enabled) {
+ this.floatingTab = new FloatingTab(this.modal, this.options.floatingTab);
+ console.log('NOSTR_LOGIN_LITE: Floating tab initialized');
+ }
+
this.initialized = true;
console.log('NOSTR_LOGIN_LITE: Initialization complete');
@@ -191,11 +1124,10 @@ class NostrLite {
launch(startScreen = 'login') {
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
- if (typeof Modal !== 'undefined') {
- const modal = new Modal(this.options);
- modal.open({ startScreen });
+ if (this.modal) {
+ this.modal.open({ startScreen });
} else {
- console.error('NOSTR_LOGIN_LITE: Modal component not available');
+ console.error('NOSTR_LOGIN_LITE: Modal not initialized - call init() first');
}
}
@@ -214,6 +1146,89 @@ class NostrLite {
}));
}
}
+
+ // CSS-only theme switching
+ switchTheme(themeName) {
+ console.log(\`NOSTR_LOGIN_LITE: Switching to \${themeName} theme\`);
+
+ if (THEME_CSS[themeName]) {
+ injectThemeCSS(themeName);
+ this.currentTheme = themeName;
+
+ // Dispatch theme change event
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new CustomEvent('nlThemeChanged', {
+ detail: { theme: themeName }
+ }));
+ }
+
+ return { theme: themeName };
+ } else {
+ console.warn(\`Theme '\${themeName}' not found, using default\`);
+ injectThemeCSS('default');
+ this.currentTheme = 'default';
+ return { theme: 'default' };
+ }
+ }
+
+ getCurrentTheme() {
+ return this.currentTheme;
+ }
+
+ getAvailableThemes() {
+ return Object.keys(THEME_CSS);
+ }
+
+ embed(container, options = {}) {
+ console.log('NOSTR_LOGIN_LITE: Creating embedded modal in container:', container);
+
+ const embedOptions = {
+ ...this.options,
+ ...options,
+ embedded: container
+ };
+
+ // Create new modal instance for embedding
+ const embeddedModal = new Modal(embedOptions);
+ embeddedModal.open();
+
+ return embeddedModal;
+ }
+
+ // Floating tab management methods
+ showFloatingTab() {
+ if (this.floatingTab) {
+ this.floatingTab.show();
+ } else {
+ console.warn('NOSTR_LOGIN_LITE: Floating tab not enabled');
+ }
+ }
+
+ hideFloatingTab() {
+ if (this.floatingTab) {
+ this.floatingTab.hide();
+ }
+ }
+
+ toggleFloatingTab() {
+ if (this.floatingTab) {
+ if (this.floatingTab.isVisible) {
+ this.floatingTab.hide();
+ } else {
+ this.floatingTab.show();
+ }
+ }
+ }
+
+ updateFloatingTab(options) {
+ if (this.floatingTab) {
+ this.floatingTab.updateOptions(options);
+ }
+ }
+
+ getFloatingTabState() {
+ return this.floatingTab ? this.floatingTab.getState() : null;
+ }
}
// Window.nostr facade for when no extension is available
@@ -267,6 +1282,21 @@ if (typeof window !== 'undefined') {
launch: (startScreen) => nostrLite.launch(startScreen),
logout: () => nostrLite.logout(),
+ // Embedded modal method
+ embed: (container, options) => nostrLite.embed(container, options),
+
+ // CSS-only theme management API
+ switchTheme: (themeName) => nostrLite.switchTheme(themeName),
+ getCurrentTheme: () => nostrLite.getCurrentTheme(),
+ getAvailableThemes: () => nostrLite.getAvailableThemes(),
+
+ // Floating tab management API
+ showFloatingTab: () => nostrLite.showFloatingTab(),
+ hideFloatingTab: () => nostrLite.hideFloatingTab(),
+ toggleFloatingTab: () => nostrLite.toggleFloatingTab(),
+ updateFloatingTab: (options) => nostrLite.updateFloatingTab(options),
+ getFloatingTabState: () => nostrLite.getFloatingTabState(),
+
// Expose for debugging
_extensionBridge: nostrLite.extensionBridge,
_instance: nostrLite
@@ -292,17 +1322,18 @@ if (typeof window !== 'undefined') {
// Check what's included
const hasModal = bundle.includes('class Modal');
const hasNostrLite = bundle.includes('NOSTR_LOGIN_LITE');
+ const hasThemeCss = bundle.includes('THEME_CSS');
console.log('\n๐ Bundle contents:');
console.log(` Modal UI: ${hasModal ? 'โ
Included' : 'โ Missing'}`);
console.log(` NOSTR_LOGIN_LITE: ${hasNostrLite ? 'โ
Included' : 'โ Missing'}`);
+ console.log(` CSS-Only Themes: ${hasThemeCss ? 'โ
Included' : 'โ Missing'}`);
console.log(` Extension Bridge: โ
Included`);
console.log(` Window.nostr facade: โ
Included`);
console.log('\n๐ Two-file architecture:');
console.log(' 1. nostr.bundle.js (official nostr-tools - 220KB)');
- console.log(' 2. nip46-extension.js (NIP-46 support - ~15KB)');
- console.log(` 3. nostr-lite.js (NOSTR_LOGIN_LITE - ${sizeKB}KB)`);
+ console.log(` 2. nostr-lite.js (NOSTR_LOGIN_LITE with CSS-only themes - ${sizeKB}KB)`);
return bundle;
}
diff --git a/lite/nostr-lite.js b/lite/nostr-lite.js
index fbe805c..133ba78 100644
--- a/lite/nostr-lite.js
+++ b/lite/nostr-lite.js
@@ -1,9 +1,14 @@
/**
* NOSTR_LOGIN_LITE - Authentication Library
+ *
+ * โ ๏ธ WARNING: THIS FILE IS AUTO-GENERATED - DO NOT EDIT MANUALLY!
+ * โ ๏ธ To make changes, edit lite/build.js and run: cd lite && node build.js
+ * โ ๏ธ Any manual edits to this file will be OVERWRITTEN when build.js runs!
+ *
* Two-file architecture:
* 1. Load nostr.bundle.js (official nostr-tools bundle)
- * 2. Load nostr-lite.js (this file - consolidated NOSTR_LOGIN_LITE library with NIP-46)
- * Generated on: 2025-09-13T18:23:00.000Z
+ * 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
+ * Generated on: 2025-09-14T17:19:15.753Z
*/
// Verify dependencies are loaded
@@ -15,25 +20,18 @@ if (typeof window !== 'undefined') {
console.log('NOSTR_LOGIN_LITE: Dependencies verified โ');
console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools));
+ console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46);
}
-// ======================================
-// NIP-46 Extension (formerly nip46-extension.js)
-// ======================================
-
-(function() {
- 'use strict';
-
- // Check if NostrTools is available
- if (typeof window.NostrTools === 'undefined') {
- console.error('NIP-46 Extension requires nostr-tools to be loaded first');
- return;
- }
-
+// ===== NIP-46 Extension Integration =====
+// Add NIP-46 functionality to NostrTools if not already present
+if (typeof window.NostrTools !== 'undefined' && !window.NostrTools.nip46) {
+ console.log('NOSTR_LOGIN_LITE: Adding NIP-46 extension to NostrTools');
+
const { nip44, generateSecretKey, getPublicKey, finalizeEvent, verifyEvent, utils } = window.NostrTools;
// NIP-05 regex for parsing
- const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$/;
+ const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(.[\w_-]+)+)$/;
const BUNKER_REGEX = /^bunker:\/\/([0-9a-f]{64})\??([?\/\w:.=&%-]*)$/;
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -521,70 +519,314 @@ if (typeof window !== 'undefined') {
SimplePool
};
- console.log('NIP-46 extension loaded successfully (embedded)');
+ console.log('NIP-46 extension loaded successfully');
console.log('Available: NostrTools.nip46');
-
-})();
-
-// Verify NIP-46 extension is now available
-if (typeof window !== 'undefined') {
- console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46);
}
// ======================================
// NOSTR_LOGIN_LITE Components
// ======================================
+// ======================================
+// CSS-Only Theme System
+// ======================================
+
+const THEME_CSS = {
+ 'default': `/**
+ * NOSTR_LOGIN_LITE - Default Monospace Theme
+ * Black/white/red color scheme with monospace typography
+ * Simplified 14-variable system (6 core + 8 floating tab)
+ */
+
+:root {
+ /* Core Variables (6) */
+ --nl-primary-color: #000000;
+ --nl-secondary-color: #ffffff;
+ --nl-accent-color: #ff0000;
+ --nl-muted-color: #666666;
+ --nl-font-family: "Courier New", Courier, monospace;
+ --nl-border-radius: 15px;
+ --nl-border-width: 3px;
+
+ /* Floating Tab Variables (8) */
+ --nl-tab-bg-logged-out: #ffffff;
+ --nl-tab-bg-logged-in: #000000;
+ --nl-tab-bg-opacity-logged-out: 0.9;
+ --nl-tab-bg-opacity-logged-in: 0.8;
+ --nl-tab-color-logged-out: #000000;
+ --nl-tab-color-logged-in: #ffffff;
+ --nl-tab-border-logged-out: #000000;
+ --nl-tab-border-logged-in: #ff0000;
+ --nl-tab-border-opacity-logged-out: 1.0;
+ --nl-tab-border-opacity-logged-in: 0.9;
+}
+
+/* Base component styles using simplified variables */
+.nl-component {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+}
+
+.nl-button {
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ font-family: var(--nl-font-family);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.nl-button:hover {
+ border-color: var(--nl-accent-color);
+}
+
+.nl-button:active {
+ background: var(--nl-accent-color);
+ color: var(--nl-secondary-color);
+}
+
+.nl-input {
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ font-family: var(--nl-font-family);
+ box-sizing: border-box;
+}
+
+.nl-input:focus {
+ border-color: var(--nl-accent-color);
+ outline: none;
+}
+
+.nl-container {
+ background: var(--nl-secondary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+}
+
+.nl-title, .nl-heading {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+ margin: 0;
+}
+
+.nl-text {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+}
+
+.nl-text--muted {
+ color: var(--nl-muted-color);
+}
+
+.nl-icon {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+}
+
+/* Floating tab styles */
+.nl-floating-tab {
+ font-family: var(--nl-font-family);
+ border-radius: var(--nl-border-radius);
+ border: var(--nl-border-width) solid;
+ transition: all 0.2s ease;
+}
+
+.nl-floating-tab--logged-out {
+ background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out));
+ color: var(--nl-tab-color-logged-out);
+ border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out));
+}
+
+.nl-floating-tab--logged-in {
+ background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in));
+ color: var(--nl-tab-color-logged-in);
+ border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in));
+}
+
+.nl-transition {
+ transition: all 0.2s ease;
+}`,
+ 'dark': `/**
+ * NOSTR_LOGIN_LITE - Dark Monospace Theme
+ */
+
+:root {
+ /* Core Variables (6) */
+ --nl-primary-color: #white;
+ --nl-secondary-color: #black;
+ --nl-accent-color: #ff0000;
+ --nl-muted-color: #666666;
+ --nl-font-family: "Courier New", Courier, monospace;
+ --nl-border-radius: 15px;
+ --nl-border-width: 3px;
+
+ /* Floating Tab Variables (8) */
+ --nl-tab-bg-logged-out: #ffffff;
+ --nl-tab-bg-logged-in: #000000;
+ --nl-tab-bg-opacity-logged-out: 0.9;
+ --nl-tab-bg-opacity-logged-in: 0.8;
+ --nl-tab-color-logged-out: #000000;
+ --nl-tab-color-logged-in: #ffffff;
+ --nl-tab-border-logged-out: #000000;
+ --nl-tab-border-logged-in: #ff0000;
+ --nl-tab-border-opacity-logged-out: 1.0;
+ --nl-tab-border-opacity-logged-in: 0.9;
+}
+
+/* Base component styles using simplified variables */
+.nl-component {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+}
+
+.nl-button {
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ font-family: var(--nl-font-family);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.nl-button:hover {
+ border-color: var(--nl-accent-color);
+}
+
+.nl-button:active {
+ background: var(--nl-accent-color);
+ color: var(--nl-secondary-color);
+}
+
+.nl-input {
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ font-family: var(--nl-font-family);
+ box-sizing: border-box;
+}
+
+.nl-input:focus {
+ border-color: var(--nl-accent-color);
+ outline: none;
+}
+
+.nl-container {
+ background: var(--nl-secondary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+}
+
+.nl-title, .nl-heading {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+ margin: 0;
+}
+
+.nl-text {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+}
+
+.nl-text--muted {
+ color: var(--nl-muted-color);
+}
+
+.nl-icon {
+ font-family: var(--nl-font-family);
+ color: var(--nl-primary-color);
+}
+
+/* Floating tab styles */
+.nl-floating-tab {
+ font-family: var(--nl-font-family);
+ border-radius: var(--nl-border-radius);
+ border: var(--nl-border-width) solid;
+ transition: all 0.2s ease;
+}
+
+.nl-floating-tab--logged-out {
+ background: rgba(255, 255, 255, var(--nl-tab-bg-opacity-logged-out));
+ color: var(--nl-tab-color-logged-out);
+ border-color: rgba(0, 0, 0, var(--nl-tab-border-opacity-logged-out));
+}
+
+.nl-floating-tab--logged-in {
+ background: rgba(0, 0, 0, var(--nl-tab-bg-opacity-logged-in));
+ color: var(--nl-tab-color-logged-in);
+ border-color: rgba(255, 0, 0, var(--nl-tab-border-opacity-logged-in));
+}
+
+.nl-transition {
+ transition: all 0.2s ease;
+}`
+};
+
+// Theme management functions
+function injectThemeCSS(themeName = 'default') {
+ if (typeof document !== 'undefined') {
+ // Remove existing theme CSS
+ const existingStyle = document.getElementById('nl-theme-css');
+ if (existingStyle) {
+ existingStyle.remove();
+ }
+
+ // Inject selected theme CSS
+ const themeCss = THEME_CSS[themeName] || THEME_CSS['default'];
+ const style = document.createElement('style');
+ style.id = 'nl-theme-css';
+ style.textContent = themeCss;
+ document.head.appendChild(style);
+ console.log(`NOSTR_LOGIN_LITE: ${themeName} theme CSS injected`);
+ }
+}
+
+// Auto-inject default theme when DOM is ready
+if (typeof document !== 'undefined') {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => injectThemeCSS('default'));
+ } else {
+ injectThemeCSS('default');
+ }
+}
+
// ======================================
// Modal UI Component
// ======================================
class Modal {
- constructor(options) {
+ constructor(options = {}) {
this.options = options;
this.container = null;
this.isVisible = false;
this.currentScreen = null;
- this.floatingTab = null;
- this.isEmbedded = false;
- this.embedContainer = null;
+ this.isEmbedded = !!options.embedded;
+ this.embeddedContainer = options.embedded;
// Initialize modal container and styles
this._initModal();
-
- // Initialize floating tab if enabled (only for floating modals)
- if (this.options?.floatingTab?.enabled && !this.isEmbedded) {
- this._initFloatingTab();
- }
}
_initModal() {
- // Check if embedded mode is requested
- if (this.options?.embedded) {
- this.isEmbedded = true;
- this.embedContainer = typeof this.options.embedded === 'string'
- ? document.querySelector(this.options.embedded)
- : this.options.embedded;
-
- if (!this.embedContainer) {
- console.error('NOSTR_LOGIN_LITE: Embed container not found:', this.options.embedded);
- return;
- }
- }
-
// Create modal container
this.container = document.createElement('div');
- this.container.id = this.isEmbedded ? 'nl-embedded-modal' : 'nl-modal';
+ this.container.id = this.isEmbedded ? 'nl-modal-embedded' : 'nl-modal';
if (this.isEmbedded) {
- // Embedded mode styles
+ // Embedded mode: inline positioning, no overlay
this.container.style.cssText = `
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ position: relative;
+ display: none;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
width: 100%;
`;
} else {
- // Floating mode styles
+ // Modal mode: fixed overlay
this.container.style.cssText = `
position: fixed;
top: 0;
@@ -594,90 +836,93 @@ class Modal {
background: rgba(0, 0, 0, 0.75);
display: none;
z-index: 10000;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
}
// Create modal content
const modalContent = document.createElement('div');
if (this.isEmbedded) {
- // Embedded content styles
- if (this.options?.seamless) {
- // Seamless mode - no borders, shadows, or background
- modalContent.style.cssText = `
- background: transparent;
- `;
- } else {
- // Standard embedded mode
- modalContent.style.cssText = `
- background: white;
- border-radius: 12px;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- overflow: hidden;
- border: 1px solid #e5e7eb;
- `;
- }
- } else {
- // Floating content styles
+ // Embedded content: no centering margin, full width
modalContent.style.cssText = `
position: relative;
- background: white;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ width: 100%;
+ border-radius: var(--nl-border-radius, 15px);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ overflow: hidden;
+ `;
+ } else {
+ // Modal content: centered with margin
+ modalContent.style.cssText = `
+ position: relative;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
width: 90%;
max-width: 400px;
margin: 50px auto;
- border-radius: 12px;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ border-radius: var(--nl-border-radius, 15px);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
max-height: 600px;
overflow: hidden;
`;
}
- // Header (optional for embedded mode)
- if (!this.isEmbedded || this.options?.showHeader !== false) {
- const modalHeader = document.createElement('div');
- modalHeader.style.cssText = `
- padding: 20px 24px 0 24px;
+ // Header
+ const modalHeader = document.createElement('div');
+ modalHeader.style.cssText = `
+ padding: 20px 24px 0 24px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: transparent;
+ border-bottom: none;
+ `;
+
+ const modalTitle = document.createElement('h2');
+ modalTitle.textContent = 'Nostr Login';
+ modalTitle.style.cssText = `
+ margin: 0;
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ `;
+
+ modalHeader.appendChild(modalTitle);
+
+ // Only add close button for non-embedded modals
+ // Embedded modals shouldn't have a close button because there's no way to reopen them
+ if (!this.isEmbedded) {
+ const closeButton = document.createElement('button');
+ closeButton.innerHTML = 'ร';
+ closeButton.onclick = () => this.close();
+ closeButton.style.cssText = `
+ background: var(--nl-secondary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ font-size: 28px;
+ color: var(--nl-primary-color);
+ cursor: pointer;
+ padding: 0;
+ width: 32px;
+ height: 32px;
display: flex;
- justify-content: space-between;
align-items: center;
+ justify-content: center;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
-
- const modalTitle = document.createElement('h2');
- modalTitle.textContent = this.options?.title || 'Nostr Login';
- modalTitle.style.cssText = `
- margin: 0;
- font-size: 24px;
- font-weight: 600;
- color: #1f2937;
- `;
-
- modalHeader.appendChild(modalTitle);
-
- // Close button (only for floating modals)
- if (!this.isEmbedded) {
- const closeButton = document.createElement('button');
- closeButton.innerHTML = 'ร';
- closeButton.onclick = () => this.close();
- closeButton.style.cssText = `
- background: none;
- border: none;
- font-size: 28px;
- color: #6b7280;
- cursor: pointer;
- padding: 0;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 6px;
- `;
- closeButton.onmouseover = () => closeButton.style.background = '#f3f4f6';
- closeButton.onmouseout = () => closeButton.style.background = 'none';
- modalHeader.appendChild(closeButton);
- }
-
- modalContent.appendChild(modalHeader);
+ closeButton.onmouseover = () => {
+ closeButton.style.borderColor = 'var(--nl-accent-color)';
+ closeButton.style.background = 'var(--nl-secondary-color)';
+ };
+ closeButton.onmouseout = () => {
+ closeButton.style.borderColor = 'var(--nl-primary-color)';
+ closeButton.style.background = 'var(--nl-secondary-color)';
+ };
+
+ modalHeader.appendChild(closeButton);
}
// Body
@@ -685,19 +930,39 @@ class Modal {
this.modalBody.style.cssText = `
padding: 24px;
overflow-y: auto;
- ${this.isEmbedded ? '' : 'max-height: 500px;'}
+ max-height: 500px;
+ background: transparent;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
+ modalContent.appendChild(modalHeader);
modalContent.appendChild(this.modalBody);
this.container.appendChild(modalContent);
- // Add to appropriate container
- if (this.isEmbedded) {
- this.embedContainer.appendChild(this.container);
+ // Add to appropriate parent
+ if (this.isEmbedded && this.embeddedContainer) {
+ // Append to specified container for embedding
+ if (typeof this.embeddedContainer === 'string') {
+ const targetElement = document.querySelector(this.embeddedContainer);
+ if (targetElement) {
+ targetElement.appendChild(this.container);
+ } else {
+ console.error('NOSTR_LOGIN_LITE: Embedded container not found:', this.embeddedContainer);
+ document.body.appendChild(this.container);
+ }
+ } else if (this.embeddedContainer instanceof HTMLElement) {
+ this.embeddedContainer.appendChild(this.container);
+ } else {
+ console.error('NOSTR_LOGIN_LITE: Invalid embedded container');
+ document.body.appendChild(this.container);
+ }
} else {
+ // Add to body for modal mode
document.body.appendChild(this.container);
-
- // Click outside to close (floating mode only)
+ }
+
+ // Click outside to close (only for modal mode)
+ if (!this.isEmbedded) {
this.container.onclick = (e) => {
if (e.target === this.container) {
this.close();
@@ -708,41 +973,16 @@ class Modal {
// Update theme
this.updateTheme();
}
-
- _initFloatingTab() {
- if (this.floatingTab) {
- this.floatingTab.destroy();
- }
-
- this.floatingTab = new FloatingTab(this, this.options.floatingTab);
- this.floatingTab.show();
-
- console.log('NOSTR_LOGIN_LITE: Floating tab initialized');
- }
updateTheme() {
- const isDark = this.options?.darkMode;
- const modalContent = this.container.querySelector(':nth-child(1)');
- const title = this.container.querySelector('h2');
-
- if (isDark) {
- modalContent.style.background = '#1f2937';
- title.style.color = 'white';
- } else {
- modalContent.style.background = 'white';
- title.style.color = '#1f2937';
- }
+ // The theme will automatically update through CSS custom properties
+ // No manual styling needed - the CSS variables handle everything
}
open(opts = {}) {
this.currentScreen = opts.startScreen;
this.isVisible = true;
-
- if (this.isEmbedded) {
- this.container.style.display = 'block';
- } else {
- this.container.style.display = 'block';
- }
+ this.container.style.display = 'block';
// Render login options
this._renderLoginOptions();
@@ -750,14 +990,8 @@ class Modal {
close() {
this.isVisible = false;
-
- if (this.isEmbedded) {
- // For embedded mode, just clear content but keep visible
- this.modalBody.innerHTML = '';
- } else {
- this.container.style.display = 'none';
- this.modalBody.innerHTML = '';
- }
+ this.container.style.display = 'none';
+ this.modalBody.innerHTML = '';
}
_renderLoginOptions() {
@@ -825,26 +1059,41 @@ class Modal {
width: 100%;
padding: 16px;
margin-bottom: 12px;
- background: ${this.options?.darkMode ? '#374151' : 'white'};
- border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'};
- border-radius: 8px;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
cursor: pointer;
transition: all 0.2s;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
button.onmouseover = () => {
- button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
+ button.style.borderColor = 'var(--nl-accent-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
button.onmouseout = () => {
- button.style.boxShadow = 'none';
+ button.style.borderColor = 'var(--nl-primary-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
const iconDiv = document.createElement('div');
- iconDiv.textContent = option.icon;
+ // Replace emoji icons with text-based ones
+ const iconMap = {
+ '๐': '[EXT]',
+ '๐': '[KEY]',
+ '๐': '[NET]',
+ '๐๏ธ': '[VIEW]',
+ '๐ฑ': '[SMS]'
+ };
+ iconDiv.textContent = iconMap[option.icon] || option.icon;
iconDiv.style.cssText = `
- font-size: 24px;
+ font-size: 16px;
+ font-weight: bold;
margin-right: 16px;
- width: 24px;
+ width: 50px;
text-align: center;
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
const contentDiv = document.createElement('div');
@@ -855,14 +1104,16 @@ class Modal {
titleDiv.style.cssText = `
font-weight: 600;
margin-bottom: 4px;
- color: ${this.options?.darkMode ? 'white' : '#1f2937'};
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
const descDiv = document.createElement('div');
descDiv.textContent = option.description;
descDiv.style.cssText = `
font-size: 14px;
- color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'};
+ color: #666666;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
contentDiv.appendChild(titleDiv);
@@ -1066,11 +1317,22 @@ class Modal {
const title = document.createElement('h3');
title.textContent = 'Choose Browser Extension';
- title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
+ title.style.cssText = `
+ margin: 0 0 16px 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ `;
const description = document.createElement('p');
description.textContent = `Found ${extensions.length} Nostr extensions. Choose which one to use:`;
- description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
+ description.style.cssText = `
+ margin-bottom: 20px;
+ color: #666666;
+ font-size: 14px;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ `;
this.modalBody.appendChild(title);
this.modalBody.appendChild(description);
@@ -1085,21 +1347,23 @@ class Modal {
width: 100%;
padding: 16px;
margin-bottom: 12px;
- background: ${this.options?.darkMode ? '#374151' : 'white'};
- border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'};
- border-radius: 8px;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
cursor: pointer;
transition: all 0.2s;
text-align: left;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
button.onmouseover = () => {
- button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
- button.style.transform = 'translateY(-1px)';
+ button.style.borderColor = 'var(--nl-accent-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
button.onmouseout = () => {
- button.style.boxShadow = 'none';
- button.style.transform = 'none';
+ button.style.borderColor = 'var(--nl-primary-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
const iconDiv = document.createElement('div');
@@ -1119,15 +1383,16 @@ class Modal {
nameDiv.style.cssText = `
font-weight: 600;
margin-bottom: 4px;
- color: ${this.options?.darkMode ? 'white' : '#1f2937'};
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
const pathDiv = document.createElement('div');
pathDiv.textContent = ext.name;
pathDiv.style.cssText = `
font-size: 12px;
- color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'};
- font-family: monospace;
+ color: #666666;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
contentDiv.appendChild(nameDiv);
@@ -1435,32 +1700,54 @@ class Modal {
this.modalBody.innerHTML = '';
const title = document.createElement('h3');
- title.textContent = 'Nostr Connect';
- title.style.cssText = 'margin: 0 0 20px 0; font-size: 18px; font-weight: 600;';
+ title.textContent = 'Connect to NIP-46 Remote Signer';
+ title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
+
+ const description = document.createElement('p');
+ description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.';
+ description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
const formGroup = document.createElement('div');
formGroup.style.cssText = 'margin-bottom: 20px;';
const label = document.createElement('label');
- label.textContent = 'Connection String:';
+ label.textContent = 'Bunker Public Key:';
label.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
const pubkeyInput = document.createElement('input');
pubkeyInput.type = 'text';
- pubkeyInput.placeholder = 'bunker://...';
+ pubkeyInput.placeholder = 'bunker://pubkey?relay=..., bunker:hex, hex, or npub...';
pubkeyInput.style.cssText = `
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
- margin-bottom: 16px;
+ margin-bottom: 12px;
font-family: monospace;
box-sizing: border-box;
`;
+ const urlLabel = document.createElement('label');
+ urlLabel.textContent = 'Remote URL (optional):';
+ urlLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
+
+ const urlInput = document.createElement('input');
+ urlInput.type = 'url';
+ urlInput.placeholder = 'ws://localhost:8080 (default)';
+ urlInput.style.cssText = `
+ width: 100%;
+ padding: 12px;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ margin-bottom: 16px;
+ box-sizing: border-box;
+ `;
+
+ // Users will enter the bunker URL manually from their bunker setup
+
const connectButton = document.createElement('button');
- connectButton.textContent = 'Connect';
- connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, null);
+ connectButton.textContent = 'Connect to Bunker';
+ connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, urlInput.value);
connectButton.style.cssText = this._getButtonStyle();
const backButton = document.createElement('button');
@@ -1470,8 +1757,11 @@ class Modal {
formGroup.appendChild(label);
formGroup.appendChild(pubkeyInput);
+ formGroup.appendChild(urlLabel);
+ formGroup.appendChild(urlInput);
this.modalBody.appendChild(title);
+ this.modalBody.appendChild(description);
this.modalBody.appendChild(formGroup);
this.modalBody.appendChild(connectButton);
this.modalBody.appendChild(backButton);
@@ -1479,15 +1769,15 @@ class Modal {
_handleNip46Connect(bunkerPubkey, bunkerUrl) {
if (!bunkerPubkey || !bunkerPubkey.length) {
- this._showError('Bunker connection string is required');
+ this._showError('Bunker pubkey is required');
return;
}
- this._showNip46Connecting(bunkerPubkey);
- this._performNip46Connect(bunkerPubkey, null);
+ this._showNip46Connecting(bunkerPubkey, bunkerUrl);
+ this._performNip46Connect(bunkerPubkey, bunkerUrl);
}
- _showNip46Connecting(bunkerPubkey) {
+ _showNip46Connecting(bunkerPubkey, bunkerUrl) {
this.modalBody.innerHTML = '';
const title = document.createElement('h3');
@@ -1495,20 +1785,19 @@ class Modal {
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600; color: #059669;';
const description = document.createElement('p');
- description.textContent = 'Establishing secure connection to your remote signer via Nostr relays.';
+ description.textContent = 'Establishing secure connection to your remote signer.';
description.style.cssText = 'margin-bottom: 20px; color: #6b7280;';
- // Show the connection string being used
- const connectionString = bunkerPubkey.length > 60 ?
- `${bunkerPubkey.substring(0, 30)}...${bunkerPubkey.substring(bunkerPubkey.length - 30)}` :
- bunkerPubkey;
+ // Normalize bunker pubkey for display (= show original format if bunker: prefix)
+ const displayPubkey = bunkerPubkey.startsWith('bunker:') || bunkerPubkey.startsWith('npub') || bunkerPubkey.length === 64 ? bunkerPubkey : bunkerPubkey;
const bunkerInfo = document.createElement('div');
bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;';
bunkerInfo.innerHTML = `
- Connecting via:
- ${connectionString}
- Using NIP-46 protocol over Nostr relays for secure communication.
+ Connecting to bunker:
+ Pubkey: ${displayPubkey}
+ Relay: ${bunkerUrl || 'ws://localhost:8080'}
+ If this relay is offline, the bunker server may be unavailable.
`;
const connectingDiv = document.createElement('div');
@@ -1628,430 +1917,8 @@ class Modal {
}
_showOtpScreen() {
- this.modalBody.innerHTML = '';
-
- const title = document.createElement('h3');
- title.textContent = 'DM/OTP Login';
- title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
-
- const description = document.createElement('p');
- description.textContent = 'Enter your public key to receive a login code via direct message.';
- description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
-
- const pubkeyInput = document.createElement('input');
- pubkeyInput.type = 'text';
- pubkeyInput.placeholder = 'Enter your public key:\nโข npub1... (bech32 format)\nโข 64-character hex string';
- pubkeyInput.style.cssText = `
- width: 100%;
- padding: 12px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- margin-bottom: 12px;
- font-family: monospace;
- font-size: 14px;
- box-sizing: border-box;
- `;
-
- // Add real-time format detection (reusing logic from local key)
- const formatHint = document.createElement('div');
- formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
-
- pubkeyInput.oninput = () => {
- const value = pubkeyInput.value.trim();
- if (!value) {
- formatHint.textContent = '';
- return;
- }
-
- const format = this._detectPubkeyFormat(value);
- if (format === 'npub') {
- formatHint.textContent = 'โ
Valid npub format detected';
- formatHint.style.color = '#059669';
- } else if (format === 'hex') {
- formatHint.textContent = 'โ
Valid hex format detected';
- formatHint.style.color = '#059669';
- } else {
- formatHint.textContent = 'โ Invalid public key format - must be npub1... or 64-character hex';
- formatHint.style.color = '#dc2626';
- }
- };
-
- const sendButton = document.createElement('button');
- sendButton.textContent = 'Send Login Code';
- sendButton.onclick = () => this._handleOtpRequest(pubkeyInput.value);
- sendButton.style.cssText = this._getButtonStyle();
-
- const backButton = document.createElement('button');
- backButton.textContent = 'Back';
- backButton.onclick = () => this._renderLoginOptions();
- backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
-
- this.modalBody.appendChild(title);
- this.modalBody.appendChild(description);
- this.modalBody.appendChild(pubkeyInput);
- this.modalBody.appendChild(formatHint);
- this.modalBody.appendChild(sendButton);
- this.modalBody.appendChild(backButton);
- }
-
- _detectPubkeyFormat(keyValue) {
- const trimmed = keyValue.trim();
-
- // Check for npub format
- if (trimmed.startsWith('npub1') && trimmed.length === 63) {
- try {
- const decoded = window.NostrTools.nip19.decode(trimmed);
- if (decoded.type === 'npub') {
- return 'npub';
- }
- } catch {
- return 'invalid';
- }
- }
-
- // Check for hex format (64 characters, valid hex)
- if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) {
- return 'hex';
- }
-
- return 'invalid';
- }
-
- _handleOtpRequest(pubkeyValue) {
- try {
- const trimmed = pubkeyValue.trim();
- if (!trimmed) {
- throw new Error('Please enter a public key');
- }
-
- const format = this._detectPubkeyFormat(trimmed);
- let pubkey, displayKey;
-
- if (format === 'npub') {
- // Decode npub format
- const decoded = window.NostrTools.nip19.decode(trimmed);
- if (decoded.type !== 'npub') {
- throw new Error('Invalid npub format');
- }
- pubkey = decoded.data; // This is the hex pubkey
- displayKey = trimmed; // Keep the original npub for display
- } else if (format === 'hex') {
- // Use hex directly as pubkey
- pubkey = trimmed;
- // Generate npub for display
- displayKey = window.NostrTools.nip19.npubEncode(pubkey);
- } else {
- throw new Error('Invalid public key format. Please enter either npub1... or 64-character hex string');
- }
-
- this._showOtpCodeScreen(pubkey, displayKey);
- } catch (error) {
- this._showError('Invalid public key: ' + error.message);
- }
- }
-
- _showOtpCodeScreen(pubkey, npub) {
- this.modalBody.innerHTML = '';
-
- const title = document.createElement('h3');
- title.textContent = 'Enter Login Code';
- title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
-
- const description = document.createElement('p');
- description.innerHTML = `Check your DMs for a login code sent to:
${npub}`;
- description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
-
- const formGroup = document.createElement('div');
- formGroup.style.cssText = 'margin-bottom: 20px;';
-
- const label = document.createElement('label');
- label.textContent = 'Login Code:';
- label.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;';
-
- const codeInput = document.createElement('input');
- codeInput.type = 'text';
- codeInput.placeholder = 'Enter 6-digit code';
- codeInput.maxLength = 6;
- codeInput.style.cssText = `
- width: 100%;
- padding: 12px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- margin-bottom: 16px;
- font-family: monospace;
- box-sizing: border-box;
- text-align: center;
- font-size: 18px;
- letter-spacing: 2px;
- `;
-
- const verifyButton = document.createElement('button');
- verifyButton.textContent = 'Verify Code';
- verifyButton.onclick = () => this._handleOtpVerification(pubkey, codeInput.value);
- verifyButton.style.cssText = this._getButtonStyle();
-
- const resendButton = document.createElement('button');
- resendButton.textContent = 'Resend Code';
- resendButton.onclick = () => this._handleOtpRequest(npub);
- resendButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px; margin-right: 8px;';
-
- const backButton = document.createElement('button');
- backButton.textContent = 'Back';
- backButton.onclick = () => this._showOtpScreen();
- backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
-
- formGroup.appendChild(label);
- formGroup.appendChild(codeInput);
-
- this.modalBody.appendChild(title);
- this.modalBody.appendChild(description);
- this.modalBody.appendChild(formGroup);
- this.modalBody.appendChild(verifyButton);
-
- const buttonRow = document.createElement('div');
- buttonRow.style.cssText = 'display: flex; gap: 8px;';
- buttonRow.appendChild(resendButton);
- buttonRow.appendChild(backButton);
- this.modalBody.appendChild(buttonRow);
-
- // Simulate sending DM (in a real implementation, this would send a DM)
- this._simulateOtpSend(pubkey);
- }
-
- async _simulateOtpSend(pubkey) {
- // Generate a random 6-digit code
- const code = Math.floor(100000 + Math.random() * 900000).toString();
-
- // Store the code temporarily (in a real implementation, this would be server-side)
- sessionStorage.setItem(`otp_${pubkey}`, code);
- sessionStorage.setItem(`otp_${pubkey}_timestamp`, Date.now().toString());
-
- console.log(`OTP/DM: Generated code ${code} for pubkey ${pubkey}`);
-
- // Actually send the DM
- await this._sendOtpDm(pubkey, code);
- }
-
- async _sendOtpDm(recipientPubkey, code) {
- try {
- console.log('๐ OTP/DM: Starting modern NIP-17 DM send process...');
-
- // Generate a temporary key pair for sending the DM
- const senderSecretKey = window.NostrTools.generateSecretKey();
- const senderPubkey = window.NostrTools.getPublicKey(senderSecretKey);
-
- console.log('๐ OTP/DM: Generated temporary sender pubkey:', senderPubkey);
- console.log('๐ค OTP/DM: Sending DM to recipient pubkey:', recipientPubkey);
-
- // Create the DM content
- const dmContent = `Your NOSTR_LOGIN_LITE login code: ${code}\n\nThis code expires in 5 minutes.`;
-
- console.log('๐ฌ OTP/DM: Original message:', dmContent);
-
- // Check if NIP-17 is available in nostr-tools
- if (!window.NostrTools?.nip17) {
- console.warn('โ ๏ธ OTP/DM: NIP-17 not available in nostr-tools, falling back to NIP-44');
- return await this._sendLegacyNip44Dm(senderSecretKey, recipientPubkey, dmContent, code);
- }
-
- // Create recipient object for NIP-17
- const recipient = {
- publicKey: recipientPubkey,
- relayUrl: undefined // Will use default relays
- };
-
- console.log('๐ OTP/DM: Creating NIP-17 gift-wrapped DM using nostr-tools...');
-
- // Use nostr-tools NIP-17 to create properly wrapped DM
- const wrappedEvent = window.NostrTools.nip17.wrapEvent(
- senderSecretKey,
- recipient,
- dmContent,
- undefined, // no conversation title
- undefined // no reply
- );
-
- console.log('๐ OTP/DM: Created NIP-17 gift wrap (kind 1059):', JSON.stringify(wrappedEvent, null, 2));
-
- // Get relays to send to
- const relays = this.options.relays || [
- 'wss://relay.damus.io',
- 'wss://relay.primal.net',
- 'wss://relay.laantungir.net',
- 'wss://nostr.mom'
- ];
-
- console.log('๐ OTP/DM: Sending NIP-17 DM to relays:', relays);
-
- // Create a pool using our enhanced SimplePool from NIP-46 extension
- const pool = new window.NostrTools.nip46.SimplePool();
-
- // Send the gift-wrapped event to all relays
- console.log('๐ก OTP/DM: === Sending NIP-17 Gift-Wrapped DM ===');
- const publishPromises = relays.map(async (relay, index) => {
- console.log(`๐ก OTP/DM: NIP-17 [${index + 1}/${relays.length}] Connecting to relay ${relay}...`);
-
- try {
- const results = await pool.publish([relay], wrappedEvent);
- console.log(`๐ก OTP/DM: NIP-17 Raw JSON response from ${relay}:`, JSON.stringify(results, null, 2));
-
- // Check if the result status is fulfilled (successful)
- const success = results.length > 0 && results[0].status === 'fulfilled';
- if (!success && results.length > 0 && results[0].status === 'rejected') {
- console.warn(`โ ๏ธ OTP/DM: NIP-17 rejected by ${relay}:`, results[0].reason);
- }
- return { relay, success, results, type: 'nip17' };
- } catch (error) {
- console.error(`โ OTP/DM: NIP-17 Failed to publish to ${relay}:`, error);
- return { relay, success: false, error: error.message, type: 'nip17' };
- }
- });
-
- // Wait for all publish attempts
- const results = await Promise.allSettled(publishPromises);
-
- const successfulResults = results.filter(result =>
- result.status === 'fulfilled' && result.value.success
- );
-
- console.log('๐ OTP/DM: Publishing summary:');
- console.log(` - Total relays attempted: ${relays.length}`);
- console.log(` - NIP-17 successful publishes: ${successfulResults.length}/${relays.length}`);
- console.log(` - Overall success rate: ${Math.round((successfulResults.length / relays.length) * 100)}%`);
-
- if (successfulResults.length > 0) {
- console.log('โ
OTP/DM: NIP-17 DM sent successfully to at least one relay');
- console.log('๐ฑ OTP/DM: Check your Nostr client for the encrypted DM');
- console.log('๐ OTP/DM: Message uses modern NIP-44 encryption with NIP-17 gift wrapping');
- console.log('๐ฏ OTP/DM: This should work with all modern Nostr clients and relays');
- } else {
- console.error('โ OTP/DM: Failed to send DM to any relays');
- console.log('๐ก OTP/DM: This might indicate relay connectivity issues or client compatibility problems');
- }
-
- // Clean up the pool safely
- try {
- console.log('๐งน OTP/DM: Cleaning up relay connections...');
- pool.close(relays);
- console.log('๐งน OTP/DM: Pool cleanup completed');
- } catch (cleanupError) {
- console.warn('โ ๏ธ OTP/DM: Pool cleanup warning (non-critical):', cleanupError.message);
- }
-
- // For demo purposes, also show the code in console
- setTimeout(() => {
- console.log(`๐ OTP/DM: Demo - Login code sent was: ${code}`);
- console.log('๐ก OTP/DM: Check your Nostr DMs or use the code above for testing');
- }, 2000);
-
- return successfulResults.length > 0;
-
- } catch (error) {
- console.error('๐ฅ OTP/DM: NIP-17 implementation error:', error);
- console.log('๐ก OTP/DM: This might indicate a problem with the nostr-tools NIP-17 implementation');
- console.log(`๐ OTP/DM: Fallback - Login code for testing: ${code}`);
- return false;
- }
- }
-
- async _sendLegacyNip44Dm(senderSecretKey, recipientPubkey, dmContent, code) {
- console.log('๐ OTP/DM: Using legacy NIP-44 direct message format');
-
- try {
- // Encrypt with NIP-44
- const conversationKey = window.NostrTools.nip44.getConversationKey(senderSecretKey, recipientPubkey);
- const nip44Content = window.NostrTools.nip44.encrypt(dmContent, conversationKey);
- console.log('๐ OTP/DM: NIP-44 encrypted length:', nip44Content.length, 'chars');
-
- // Create NIP-44 DM event (modern approach - no custom tags)
- const nip44Event = {
- kind: 4,
- created_at: Math.floor(Date.now() / 1000),
- tags: [
- ['p', recipientPubkey]
- ],
- content: nip44Content
- };
-
- // Sign the event
- const signedEvent = window.NostrTools.finalizeEvent(nip44Event, senderSecretKey);
- console.log('๐ OTP/DM: NIP-44 signed event:', JSON.stringify(signedEvent, null, 2));
-
- // Get relays to send to
- const relays = this.options.relays || [
- 'wss://relay.damus.io',
- 'wss://relay.primal.net',
- 'wss://relay.laantungir.net',
- 'wss://nostr.mom'
- ];
-
- // Create a pool and send
- const pool = new window.NostrTools.nip46.SimplePool();
- const publishPromises = relays.map(async (relay, index) => {
- console.log(`๐ก OTP/DM: Legacy NIP-44 [${index + 1}/${relays.length}] Connecting to relay ${relay}...`);
-
- try {
- const results = await pool.publish([relay], signedEvent);
- console.log(`๐ก OTP/DM: Legacy NIP-44 Raw JSON response from ${relay}:`, JSON.stringify(results, null, 2));
-
- const success = results.length > 0 && results[0].status === 'fulfilled';
- if (!success && results.length > 0 && results[0].status === 'rejected') {
- console.warn(`โ ๏ธ OTP/DM: Legacy NIP-44 rejected by ${relay}:`, results[0].reason);
- }
- return { relay, success, results, type: 'legacy-nip44' };
- } catch (error) {
- console.error(`โ OTP/DM: Legacy NIP-44 Failed to publish to ${relay}:`, error);
- return { relay, success: false, error: error.message, type: 'legacy-nip44' };
- }
- });
-
- const results = await Promise.allSettled(publishPromises);
- const successfulResults = results.filter(result =>
- result.status === 'fulfilled' && result.value.success
- );
-
- console.log('๐ OTP/DM: Legacy publishing summary:');
- console.log(` - Legacy NIP-44 successful publishes: ${successfulResults.length}/${relays.length}`);
-
- pool.close(relays);
- return successfulResults.length > 0;
-
- } catch (error) {
- console.error('๐ฅ OTP/DM: Legacy NIP-44 error:', error);
- console.log(`๐ OTP/DM: Fallback - Login code for testing: ${code}`);
- return false;
- }
- }
-
- _handleOtpVerification(pubkey, enteredCode) {
- if (!enteredCode || enteredCode.length !== 6) {
- this._showError('Please enter a 6-digit code');
- return;
- }
-
- const storedCode = sessionStorage.getItem(`otp_${pubkey}`);
- const timestamp = parseInt(sessionStorage.getItem(`otp_${pubkey}_timestamp`) || '0');
-
- // Check if code is expired (5 minutes)
- if (Date.now() - timestamp > 5 * 60 * 1000) {
- sessionStorage.removeItem(`otp_${pubkey}`);
- sessionStorage.removeItem(`otp_${pubkey}_timestamp`);
- this._showError('Login code has expired. Please request a new one.');
- return;
- }
-
- if (enteredCode === storedCode) {
- // Clean up stored code
- sessionStorage.removeItem(`otp_${pubkey}`);
- sessionStorage.removeItem(`otp_${pubkey}_timestamp`);
-
- console.log('OTP/DM: Code verified successfully');
-
- // Set as read-only authentication (since we don't have the private key)
- this._setAuthMethod('readonly', { pubkey });
- } else {
- this._showError('Invalid login code. Please check and try again.');
- }
+ // Placeholder for OTP functionality
+ this._showError('OTP/DM not yet implemented - coming soon!');
}
_getButtonStyle(type = 'primary') {
@@ -2059,23 +1926,24 @@ class Modal {
display: block;
width: 100%;
padding: 12px;
- border: none;
- border-radius: 8px;
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
if (type === 'primary') {
return baseStyle + `
- background: #3b82f6;
- color: white;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
`;
} else {
return baseStyle + `
- background: #6b7280;
- color: white;
+ background: #cccccc;
+ color: var(--nl-primary-color);
`;
}
}
@@ -2090,32 +1958,6 @@ class Modal {
static getInstance() {
return Modal.instance;
}
-
- // Floating tab methods
- showFloatingTab() {
- if (this.floatingTab) {
- this.floatingTab.show();
- }
- }
-
- hideFloatingTab() {
- if (this.floatingTab) {
- this.floatingTab.hide();
- }
- }
-
- updateFloatingTab(options) {
- if (this.floatingTab) {
- this.floatingTab.updateOptions(options);
- }
- }
-
- destroyFloatingTab() {
- if (this.floatingTab) {
- this.floatingTab.destroy();
- this.floatingTab = null;
- }
- }
}
// Initialize global instance
@@ -2127,7 +1969,7 @@ window.addEventListener('load', () => {
// ======================================
-// Floating Tab Component
+// FloatingTab Component (Recovered from git history)
// ======================================
class FloatingTab {
@@ -2135,13 +1977,13 @@ class FloatingTab {
this.modal = modal;
this.options = {
enabled: true,
- hPosition: 1.0, // 100% from left (right edge) - can be decimal 0.0-1.0 or percentage '95%'
- vPosition: 0.5, // 50% from top (center) - can be decimal 0.0-1.0 or percentage '50%'
+ hPosition: 1.0, // 0.0 = left, 1.0 = right
+ vPosition: 0.5, // 0.0 = top, 1.0 = bottom
offset: { x: 0, y: 0 },
appearance: {
- style: 'pill',
- theme: 'auto',
- icon: '๐',
+ style: 'pill', // 'pill', 'square', 'circle'
+ theme: 'auto', // 'auto', 'light', 'dark'
+ icon: '[LOGIN]',
text: 'Login',
iconOnly: false
},
@@ -2151,428 +1993,310 @@ class FloatingTab {
autoSlide: true,
persistent: false
},
- animation: {
- slideDistance: '80%',
- slideDirection: 'auto', // 'auto', 'left', 'right', 'up', 'down'
- duration: '300ms',
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
- },
...options
};
-
- this.container = null;
- this.isVisible = false;
+
this.isAuthenticated = false;
this.userInfo = null;
-
- this._init();
+ this.container = null;
+ this.isVisible = false;
+
+ if (this.options.enabled) {
+ this._init();
+ }
}
-
+
_init() {
+ console.log('FloatingTab: Initializing with options:', this.options);
this._createContainer();
- this._attachEventListeners();
+ this._setupEventListeners();
this._updateAppearance();
+ this._position();
+ this.show();
}
-
+
_createContainer() {
+ // Remove existing floating tab if any
+ const existingTab = document.getElementById('nl-floating-tab');
+ if (existingTab) {
+ existingTab.remove();
+ }
+
this.container = document.createElement('div');
- this.container.className = 'nl-floating-tab';
this.container.id = 'nl-floating-tab';
+ this.container.className = 'nl-floating-tab';
- // Set CSS custom properties for animations
- this.container.style.setProperty('--animation-duration', this.options.animation.duration);
- this.container.style.setProperty('--animation-easing', this.options.animation.easing);
- this.container.style.setProperty('--slide-distance', this.options.animation.slideDistance);
-
- // Base positioning styles
- this.container.style.cssText += `
+ // Base styles - positioning and behavior
+ this.container.style.cssText = `
position: fixed;
- z-index: 9998;
+ z-index: 9999;
cursor: pointer;
- transition: transform var(--animation-duration) var(--animation-easing);
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
user-select: none;
- -webkit-user-select: none;
- `;
-
- this._updatePosition();
- this._updateStyle();
-
- document.body.appendChild(this.container);
- }
-
- _updatePosition() {
- const { hPosition, vPosition, offset } = this.options;
-
- // Reset positioning
- this.container.style.left = '';
- this.container.style.right = '';
- this.container.style.top = '';
- this.container.style.bottom = '';
- this.container.style.transform = '';
-
- // Parse position values (handle both decimal and percentage)
- const hPos = this._parsePositionValue(hPosition);
- const vPos = this._parsePositionValue(vPosition);
-
- // Horizontal positioning
- this.container.style.left = `calc(${hPos * 100}% + ${offset.x}px)`;
-
- // Vertical positioning
- this.container.style.top = `calc(${vPos * 100}% + ${offset.y}px)`;
-
- // Center the element on its position
- this.container.style.transform = 'translate(-50%, -50%)';
-
- // Update CSS classes for styling context
- if (hPos < 0.5) {
- this.container.classList.add('nl-floating-tab--left');
- this.container.classList.remove('nl-floating-tab--right');
- } else {
- this.container.classList.add('nl-floating-tab--right');
- this.container.classList.remove('nl-floating-tab--left');
- }
-
- // Initial slide-out state
- this._updateSlideState(false);
- }
-
- _parsePositionValue(value) {
- if (typeof value === 'string' && value.endsWith('%')) {
- return parseFloat(value) / 100;
- }
- return Math.max(0, Math.min(1, parseFloat(value) || 0));
- }
-
- _updateStyle() {
- const { appearance } = this.options;
- const isDark = this._isDarkMode();
-
- // Base styles
- let baseStyles = `
display: flex;
align-items: center;
- padding: 12px 16px;
- border: none;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
+ justify-content: center;
+ transition: all 0.2s ease;
+ font-size: 14px;
+ font-weight: 500;
+ padding: 8px 16px;
+ min-width: 80px;
+ max-width: 200px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
`;
-
- // Style-specific modifications
- switch (appearance.style) {
- case 'pill':
- if (this.options.position === 'left') {
- baseStyles += `border-radius: 0 25px 25px 0;`;
- } else {
- baseStyles += `border-radius: 25px 0 0 25px;`;
- }
- break;
- case 'square':
- if (this.options.position === 'left') {
- baseStyles += `border-radius: 0 8px 8px 0;`;
- } else {
- baseStyles += `border-radius: 8px 0 0 8px;`;
- }
- break;
- case 'circle':
- baseStyles += `
- border-radius: 50%;
- width: 48px;
- height: 48px;
- padding: 12px;
- justify-content: center;
- `;
- break;
- case 'minimal':
- baseStyles += `
- border-radius: 4px;
- padding: 8px 12px;
- `;
- break;
- }
-
- // Theme colors
- if (isDark) {
- baseStyles += `
- background: rgba(31, 41, 55, 0.95);
- color: white;
- border: 1px solid rgba(75, 85, 99, 0.8);
- `;
- } else {
- baseStyles += `
- background: rgba(255, 255, 255, 0.95);
- color: #1f2937;
- border: 1px solid rgba(209, 213, 219, 0.8);
- `;
- }
-
- this.container.style.cssText += baseStyles;
+
+ document.body.appendChild(this.container);
}
-
- _updateAppearance() {
- const { appearance } = this.options;
-
- // Clear existing content
- this.container.innerHTML = '';
-
- if (this.isAuthenticated && this.options.behavior.showUserInfo && this.userInfo) {
- this._renderAuthenticatedState();
- } else {
- this._renderUnauthenticatedState();
- }
- }
-
- _renderUnauthenticatedState() {
- const { appearance } = this.options;
-
- // Icon
- if (appearance.icon) {
- const iconEl = document.createElement('div');
- iconEl.textContent = appearance.icon;
- iconEl.style.cssText = `
- font-size: 18px;
- ${appearance.iconOnly || appearance.style === 'circle' ? '' : 'margin-right: 8px;'}
- `;
- this.container.appendChild(iconEl);
- }
-
- // Text (unless icon-only or circle style)
- if (!appearance.iconOnly && appearance.style !== 'circle' && appearance.text) {
- const textEl = document.createElement('span');
- textEl.textContent = appearance.text;
- textEl.style.cssText = `
- font-size: 14px;
- font-weight: 500;
- white-space: nowrap;
- `;
- this.container.appendChild(textEl);
- }
- }
-
- _renderAuthenticatedState() {
- const iconEl = document.createElement('div');
- iconEl.textContent = '๐ช';
- iconEl.style.cssText = `
- font-size: 18px;
- ${this.options.appearance.style === 'circle' ? '' : 'margin-right: 8px;'}
- `;
- this.container.appendChild(iconEl);
-
- if (this.options.appearance.style !== 'circle') {
- const textEl = document.createElement('span');
- if (this.userInfo) {
- const displayName = this.userInfo.name || this.userInfo.display_name || 'User';
- textEl.textContent = `Logout (${displayName.length > 8 ? displayName.substring(0, 8) + '...' : displayName})`;
- } else {
- textEl.textContent = 'Logout';
- }
- textEl.style.cssText = `
- font-size: 14px;
- font-weight: 500;
- white-space: nowrap;
- `;
- this.container.appendChild(textEl);
- }
- }
-
- _attachEventListeners() {
+
+ _setupEventListeners() {
+ if (!this.container) return;
+
// Click handler
this.container.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
-
- if (this.isAuthenticated && this.options.behavior.showUserInfo) {
- // Logout when authenticated
- if (typeof window !== 'undefined' && window.NOSTR_LOGIN_LITE) {
- window.NOSTR_LOGIN_LITE.logout();
- }
- this._dispatchEvent('nlFloatingTabUserClick', { userInfo: this.userInfo });
- } else {
- // Open login modal when not authenticated
- this.modal.open();
- this._dispatchEvent('nlFloatingTabClick', {});
- }
+ this._handleClick();
});
-
- // Hover effects for auto-slide
- if (this.options.behavior.autoSlide) {
- this.container.addEventListener('mouseenter', () => {
- this._updateSlideState(true);
- });
-
- this.container.addEventListener('mouseleave', () => {
- this._updateSlideState(false);
- });
- }
-
- // Authentication event listeners
- window.addEventListener('nlAuth', (event) => {
- this.updateAuthState(true, event.detail);
- });
-
- window.addEventListener('nlLogout', () => {
- this.updateAuthState(false, null);
- });
-
- // Responsive updates
- window.addEventListener('resize', () => {
- this._handleResize();
- });
- }
-
- _updateSlideState(isHovered) {
- if (!this.options.behavior.autoSlide) return;
-
- const { hPosition, vPosition, animation } = this.options;
- const { slideDistance, slideDirection } = animation;
-
- // Parse positions
- const hPos = this._parsePositionValue(hPosition);
- const vPos = this._parsePositionValue(vPosition);
-
- // Determine slide direction
- let direction = slideDirection;
- if (direction === 'auto') {
- // Auto-detect based on position
- if (hPos < 0.25) direction = 'left';
- else if (hPos > 0.75) direction = 'right';
- else if (vPos < 0.25) direction = 'up';
- else if (vPos > 0.75) direction = 'down';
- else direction = hPos < 0.5 ? 'left' : 'right'; // Default to horizontal
- }
-
- // Base transform (centering)
- let transform = 'translate(-50%, -50%)';
-
- if (!isHovered) {
- // Add slide offset based on direction
- switch (direction) {
- case 'left':
- transform += ` translateX(calc(-1 * ${slideDistance}))`;
- break;
- case 'right':
- transform += ` translateX(${slideDistance})`;
- break;
- case 'up':
- transform += ` translateY(calc(-1 * ${slideDistance}))`;
- break;
- case 'down':
- transform += ` translateY(${slideDistance})`;
- break;
- }
- }
-
- this.container.style.transform = transform.trim();
- }
-
- _handleResize() {
- // Update positioning on window resize
- this._updatePosition();
-
- // Handle responsive design
- const width = window.innerWidth;
- if (width < 768) {
- // Mobile: force icon-only mode
- this._setResponsiveMode('mobile');
- } else if (width < 1024) {
- // Tablet: abbreviated text
- this._setResponsiveMode('tablet');
- } else {
- // Desktop: full text
- this._setResponsiveMode('desktop');
- }
- }
-
- _setResponsiveMode(mode) {
- const originalIconOnly = this.options.appearance.iconOnly;
-
- switch (mode) {
- case 'mobile':
- this.options.appearance.iconOnly = true;
- break;
- case 'tablet':
- this.options.appearance.iconOnly = originalIconOnly;
- if (this.options.appearance.text && this.options.appearance.text.length > 8) {
- // Abbreviate text on tablet
- this.options.appearance.text = this.options.appearance.text.substring(0, 6) + '...';
- }
- break;
- case 'desktop':
- // Restore original settings
- break;
- }
-
- this._updateAppearance();
- }
-
- _isDarkMode() {
- const { theme } = this.options.appearance;
-
- if (theme === 'dark') return true;
- if (theme === 'light') return false;
-
- // Auto-detect
- if (this.modal && this.modal.options && this.modal.options.darkMode) {
- return this.modal.options.darkMode;
- }
-
- return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
- }
-
- _dispatchEvent(eventName, detail) {
- if (typeof window !== 'undefined') {
- window.dispatchEvent(new CustomEvent(eventName, { detail }));
- }
- }
-
- // Public API
- show() {
- if (this.container && !this.isVisible) {
- this.container.style.display = 'flex';
- this.isVisible = true;
-
- // Trigger initial slide state
+
+ // Hover effects
+ this.container.addEventListener('mouseenter', () => {
if (this.options.behavior.autoSlide) {
- setTimeout(() => this._updateSlideState(false), 100);
+ this._slideIn();
+ }
+ });
+
+ this.container.addEventListener('mouseleave', () => {
+ if (this.options.behavior.autoSlide) {
+ this._slideOut();
+ }
+ });
+
+ // Listen for authentication events
+ window.addEventListener('nlMethodSelected', (e) => {
+ console.log('FloatingTab: Authentication method selected:', e.detail);
+ this._handleAuth(e.detail);
+ });
+
+ window.addEventListener('nlLogout', () => {
+ console.log('FloatingTab: Logout detected');
+ this._handleLogout();
+ });
+ }
+
+ _handleClick() {
+ console.log('FloatingTab: Clicked');
+
+ if (this.isAuthenticated && this.options.behavior.showUserInfo) {
+ // Show user menu or profile options
+ this._showUserMenu();
+ } else {
+ // Open login modal
+ if (this.modal) {
+ this.modal.open({ startScreen: 'login' });
}
}
}
-
- hide() {
- if (this.container && this.isVisible) {
- this.container.style.display = 'none';
- this.isVisible = false;
- }
- }
-
- updateOptions(newOptions) {
- this.options = { ...this.options, ...newOptions };
- this._updatePosition();
- this._updateStyle();
- this._updateAppearance();
- }
-
- updateAuthState(isAuthenticated, userInfo = null) {
- this.isAuthenticated = isAuthenticated;
- this.userInfo = userInfo;
+
+ _handleAuth(authData) {
+ console.log('FloatingTab: Handling authentication:', authData);
+ this.isAuthenticated = true;
+ this.userInfo = authData;
- if (isAuthenticated && this.options.behavior.hideWhenAuthenticated) {
+ if (this.options.behavior.hideWhenAuthenticated) {
this.hide();
} else {
this._updateAppearance();
- if (!this.isVisible) {
- this.show();
- }
}
}
-
+
+ _handleLogout() {
+ console.log('FloatingTab: Handling logout');
+ this.isAuthenticated = false;
+ this.userInfo = null;
+
+ if (this.options.behavior.hideWhenAuthenticated) {
+ this.show();
+ }
+
+ this._updateAppearance();
+ }
+
+ _showUserMenu() {
+ // Simple user menu - could be expanded
+ const menu = document.createElement('div');
+ menu.style.cssText = `
+ position: fixed;
+ background: var(--nl-secondary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ padding: 12px;
+ z-index: 10000;
+ font-family: var(--nl-font-family);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ `;
+
+ // Position near the floating tab
+ const tabRect = this.container.getBoundingClientRect();
+ if (this.options.hPosition > 0.5) {
+ // Tab is on right side, show menu to the left
+ menu.style.right = (window.innerWidth - tabRect.left) + 'px';
+ } else {
+ // Tab is on left side, show menu to the right
+ menu.style.left = tabRect.right + 'px';
+ }
+ menu.style.top = tabRect.top + 'px';
+
+ // Menu content
+ const userDisplay = this.userInfo?.pubkey ?
+ `${this.userInfo.pubkey.slice(0, 8)}...${this.userInfo.pubkey.slice(-4)}` :
+ 'Authenticated';
+
+ menu.innerHTML = `
+ ${userDisplay}
+
+ `;
+
+ document.body.appendChild(menu);
+
+ // Auto-remove menu after delay or on outside click
+ const removeMenu = () => menu.remove();
+ setTimeout(removeMenu, 5000);
+
+ document.addEventListener('click', function onOutsideClick(e) {
+ if (!menu.contains(e.target) && e.target !== this.container) {
+ removeMenu();
+ document.removeEventListener('click', onOutsideClick);
+ }
+ });
+ }
+
+ _updateAppearance() {
+ if (!this.container) return;
+
+ // Update content
+ if (this.isAuthenticated && this.options.behavior.showUserInfo) {
+ const display = this.userInfo?.pubkey ?
+ (this.options.appearance.iconOnly ?
+ '[USER]' :
+ `[USER] ${this.userInfo.pubkey.slice(0, 6)}...`) :
+ (this.options.appearance.iconOnly ? '[AUTH]' : '[AUTH] Logged In');
+
+ this.container.textContent = display;
+ this.container.className = 'nl-floating-tab nl-floating-tab--logged-in';
+ } else {
+ const display = this.options.appearance.iconOnly ?
+ this.options.appearance.icon :
+ `${this.options.appearance.icon} ${this.options.appearance.text}`;
+
+ this.container.textContent = display;
+ this.container.className = 'nl-floating-tab nl-floating-tab--logged-out';
+ }
+
+ // Apply appearance styles based on current state
+ this._applyThemeStyles();
+ }
+
+ _applyThemeStyles() {
+ if (!this.container) return;
+
+ // The CSS classes will handle the theming through CSS custom properties
+ // Additional style customizations can be added here if needed
+
+ // Apply style variant
+ if (this.options.appearance.style === 'circle') {
+ this.container.style.borderRadius = '50%';
+ this.container.style.width = '48px';
+ this.container.style.height = '48px';
+ this.container.style.minWidth = '48px';
+ this.container.style.padding = '0';
+ } else if (this.options.appearance.style === 'square') {
+ this.container.style.borderRadius = '4px';
+ } else {
+ // pill style (default)
+ this.container.style.borderRadius = 'var(--nl-border-radius)';
+ }
+ }
+
+ _position() {
+ if (!this.container) return;
+
+ const padding = 16; // Distance from screen edge
+
+ // Calculate position based on percentage
+ const x = this.options.hPosition * (window.innerWidth - this.container.offsetWidth - padding * 2) + padding + this.options.offset.x;
+ const y = this.options.vPosition * (window.innerHeight - this.container.offsetHeight - padding * 2) + padding + this.options.offset.y;
+
+ this.container.style.left = `${x}px`;
+ this.container.style.top = `${y}px`;
+
+ console.log(`FloatingTab: Positioned at (${x}, ${y})`);
+ }
+
+ _slideIn() {
+ if (!this.container || !this.options.behavior.autoSlide) return;
+
+ // Slide towards center slightly
+ const currentTransform = this.container.style.transform || '';
+ if (this.options.hPosition > 0.5) {
+ this.container.style.transform = currentTransform + ' translateX(-8px)';
+ } else {
+ this.container.style.transform = currentTransform + ' translateX(8px)';
+ }
+ }
+
+ _slideOut() {
+ if (!this.container || !this.options.behavior.autoSlide) return;
+
+ // Reset position
+ this.container.style.transform = '';
+ }
+
+ show() {
+ if (!this.container) return;
+ this.container.style.display = 'flex';
+ this.isVisible = true;
+ console.log('FloatingTab: Shown');
+ }
+
+ hide() {
+ if (!this.container) return;
+ this.container.style.display = 'none';
+ this.isVisible = false;
+ console.log('FloatingTab: Hidden');
+ }
+
destroy() {
if (this.container) {
this.container.remove();
this.container = null;
}
this.isVisible = false;
+ console.log('FloatingTab: Destroyed');
+ }
+
+ // Update options and re-apply
+ updateOptions(newOptions) {
+ this.options = { ...this.options, ...newOptions };
+ if (this.container) {
+ this._updateAppearance();
+ this._position();
+ }
+ }
+
+ // Get current state
+ getState() {
+ return {
+ isVisible: this.isVisible,
+ isAuthenticated: this.isAuthenticated,
+ userInfo: this.userInfo,
+ options: this.options
+ };
}
}
@@ -2634,14 +2358,16 @@ class NostrLite {
this.options = {};
this.extensionBridge = new ExtensionBridge();
this.initialized = false;
+ this.currentTheme = 'default';
+ this.modal = null;
+ this.floatingTab = null;
}
async init(options = {}) {
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
- this.options = this._deepMerge({
- theme: 'light',
- darkMode: false,
+ this.options = {
+ theme: 'default',
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
methods: {
extension: true,
@@ -2652,13 +2378,13 @@ class NostrLite {
},
floatingTab: {
enabled: false,
- hPosition: 1.0, // 100% from left (right edge)
- vPosition: 0.5, // 50% from top (center)
+ hPosition: 1.0,
+ vPosition: 0.5,
offset: { x: 0, y: 0 },
appearance: {
style: 'pill',
theme: 'auto',
- icon: '๐',
+ icon: '[LOGIN]',
text: 'Login',
iconOnly: false
},
@@ -2667,49 +2393,35 @@ class NostrLite {
showUserInfo: true,
autoSlide: true,
persistent: false
- },
- animation: {
- slideDistance: '80%',
- slideDirection: 'auto', // 'auto', 'left', 'right', 'up', 'down'
- duration: '300ms',
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
}
- }
- }, options);
+ },
+ ...options
+ };
+
+ // Apply the selected theme (CSS-only)
+ this.switchTheme(this.options.theme);
// Set up window.nostr facade if no extension detected
if (this.extensionBridge.getExtensionCount() === 0) {
this._setupWindowNostrFacade();
}
+ // Create modal during init (matching original git architecture)
+ this.modal = new Modal(this.options);
+ console.log('NOSTR_LOGIN_LITE: Modal created during init');
+
+ // Initialize floating tab if enabled
+ if (this.options.floatingTab.enabled) {
+ this.floatingTab = new FloatingTab(this.modal, this.options.floatingTab);
+ console.log('NOSTR_LOGIN_LITE: Floating tab initialized');
+ }
+
this.initialized = true;
console.log('NOSTR_LOGIN_LITE: Initialization complete');
- // Set up event listeners for authentication flow
- this._setupAuthEventHandlers();
-
- // Initialize modal with floating tab support
- this.modal = new Modal(this.options);
-
return this;
}
- _deepMerge(target, source) {
- const result = { ...target };
-
- for (const key in source) {
- if (source.hasOwnProperty(key)) {
- if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
- result[key] = this._deepMerge(target[key] || {}, source[key]);
- } else {
- result[key] = source[key];
- }
- }
- }
-
- return result;
- }
-
_setupWindowNostrFacade() {
if (typeof window !== 'undefined' && !window.nostr) {
window.nostr = new WindowNostr(this);
@@ -2717,187 +2429,13 @@ class NostrLite {
}
}
- _setupAuthEventHandlers() {
- if (typeof window === 'undefined') return;
-
- // Listen for authentication method selection
- window.addEventListener('nlMethodSelected', async (event) => {
- console.log('NOSTR_LOGIN_LITE: Authentication method selected:', event.detail);
-
- const { method, pubkey, signer, extension, secret } = event.detail;
-
- try {
- // Complete the authentication flow
- await this._completeAuthentication(method, event.detail);
- } catch (error) {
- console.error('NOSTR_LOGIN_LITE: Authentication completion failed:', error);
- this._dispatchAuthEvent('nlAuthFailed', { error: error.message });
- }
- });
- }
-
- async _completeAuthentication(method, authData) {
- console.log('NOSTR_LOGIN_LITE: Completing authentication for method:', method);
-
- const authResult = {
- method,
- pubkey: authData.pubkey,
- timestamp: Date.now()
- };
-
- // Add method-specific data
- switch (method) {
- case 'extension':
- authResult.extension = authData.extension;
- break;
- case 'local':
- authResult.secret = authData.secret;
- break;
- case 'nip46':
- authResult.signer = authData.signer;
- authResult.remotePubkey = authData.signer.remotePubkey;
- break;
- case 'readonly':
- // No additional data needed
- break;
- }
-
- // Store authentication state
- if (typeof localStorage !== 'undefined') {
- localStorage.setItem('nl_current', JSON.stringify({
- method,
- pubkey: authData.pubkey,
- timestamp: Date.now()
- }));
- }
-
- console.log('NOSTR_LOGIN_LITE: Authentication completed successfully');
- console.log('NOSTR_LOGIN_LITE: User pubkey:', authData.pubkey);
-
- // Fetch and display profile information
- await this._fetchAndDisplayProfile(authData.pubkey);
-
- // Dispatch success event
- this._dispatchAuthEvent('nlAuth', authResult);
- }
-
- async _fetchAndDisplayProfile(pubkey) {
- console.log('NOSTR_LOGIN_LITE: Fetching profile for pubkey:', pubkey);
-
- try {
- // Create a simple pool for fetching profile
- const pool = new window.NostrTools.SimplePool();
- const relays = this.options.relays || ['wss://relay.damus.io', 'wss://nos.lol'];
-
- console.log('NOSTR_LOGIN_LITE: Querying relays for profile:', relays);
-
- // Fetch profile metadata (kind 0)
- const profileEvents = await pool.querySync(relays, {
- kinds: [0],
- authors: [pubkey],
- limit: 1
- });
-
- pool.close(relays);
-
- if (profileEvents && profileEvents.length > 0) {
- const profileEvent = profileEvents[0];
- const profile = JSON.parse(profileEvent.content);
-
- console.log('NOSTR_LOGIN_LITE: Profile fetched successfully:', profile);
-
- // Display profile information
- this._displayProfileInfo(pubkey, profile);
- } else {
- console.log('NOSTR_LOGIN_LITE: No profile found, displaying pubkey only');
- this._displayProfileInfo(pubkey, null);
- }
-
- } catch (error) {
- console.error('NOSTR_LOGIN_LITE: Failed to fetch profile:', error);
- this._displayProfileInfo(pubkey, null);
- }
- }
-
- _displayProfileInfo(pubkey, profile) {
- console.log('NOSTR_LOGIN_LITE: Displaying profile info');
-
- // Create or update profile display
- let profileDiv = document.getElementById('nl-profile-info');
- if (!profileDiv) {
- profileDiv = document.createElement('div');
- profileDiv.id = 'nl-profile-info';
- profileDiv.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- padding: 16px;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- z-index: 9999;
- max-width: 300px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- `;
- document.body.appendChild(profileDiv);
- }
-
- const npub = window.NostrTools.nip19.npubEncode(pubkey);
- const shortPubkey = `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
-
- profileDiv.innerHTML = `
-
-
โ
-
-
Logged In
-
NOSTR_LOGIN_LITE
-
-
-
- ${profile ? `
-
-
${profile.name || 'Anonymous'}
- ${profile.about ? `
${profile.about.slice(0, 100)}${profile.about.length > 100 ? '...' : ''}
` : ''}
-
- ` : ''}
-
-
- Pubkey: ${shortPubkey}
-
-
-
- npub: ${npub.slice(0, 12)}...${npub.slice(-8)}
-
-
-
- `;
-
- console.log('NOSTR_LOGIN_LITE: Profile display updated');
- }
-
- _dispatchAuthEvent(eventName, data) {
- if (typeof window !== 'undefined') {
- window.dispatchEvent(new CustomEvent(eventName, {
- detail: data
- }));
- console.log('NOSTR_LOGIN_LITE: Dispatched event:', eventName, data);
- }
- }
-
launch(startScreen = 'login') {
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
if (this.modal) {
this.modal.open({ startScreen });
- } else if (typeof Modal !== 'undefined') {
- this.modal = new Modal(this.options);
- this.modal.open({ startScreen });
} else {
- console.error('NOSTR_LOGIN_LITE: Modal component not available');
+ console.error('NOSTR_LOGIN_LITE: Modal not initialized - call init() first');
}
}
@@ -2917,32 +2455,41 @@ class NostrLite {
}
}
- // Floating tab methods
- showFloatingTab() {
- if (this.modal) {
- this.modal.showFloatingTab();
+ // CSS-only theme switching
+ switchTheme(themeName) {
+ console.log(`NOSTR_LOGIN_LITE: Switching to ${themeName} theme`);
+
+ if (THEME_CSS[themeName]) {
+ injectThemeCSS(themeName);
+ this.currentTheme = themeName;
+
+ // Dispatch theme change event
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new CustomEvent('nlThemeChanged', {
+ detail: { theme: themeName }
+ }));
+ }
+
+ return { theme: themeName };
+ } else {
+ console.warn(`Theme '${themeName}' not found, using default`);
+ injectThemeCSS('default');
+ this.currentTheme = 'default';
+ return { theme: 'default' };
}
}
- hideFloatingTab() {
- if (this.modal) {
- this.modal.hideFloatingTab();
- }
+ getCurrentTheme() {
+ return this.currentTheme;
}
- updateFloatingTab(options) {
- if (this.modal) {
- this.modal.updateFloatingTab(options);
- }
- }
-
- destroyFloatingTab() {
- if (this.modal) {
- this.modal.destroyFloatingTab();
- }
+ getAvailableThemes() {
+ return Object.keys(THEME_CSS);
}
embed(container, options = {}) {
+ console.log('NOSTR_LOGIN_LITE: Creating embedded modal in container:', container);
+
const embedOptions = {
...this.options,
...options,
@@ -2955,6 +2502,41 @@ class NostrLite {
return embeddedModal;
}
+
+ // Floating tab management methods
+ showFloatingTab() {
+ if (this.floatingTab) {
+ this.floatingTab.show();
+ } else {
+ console.warn('NOSTR_LOGIN_LITE: Floating tab not enabled');
+ }
+ }
+
+ hideFloatingTab() {
+ if (this.floatingTab) {
+ this.floatingTab.hide();
+ }
+ }
+
+ toggleFloatingTab() {
+ if (this.floatingTab) {
+ if (this.floatingTab.isVisible) {
+ this.floatingTab.hide();
+ } else {
+ this.floatingTab.show();
+ }
+ }
+ }
+
+ updateFloatingTab(options) {
+ if (this.floatingTab) {
+ this.floatingTab.updateOptions(options);
+ }
+ }
+
+ getFloatingTabState() {
+ return this.floatingTab ? this.floatingTab.getState() : null;
+ }
}
// Window.nostr facade for when no extension is available
@@ -3011,11 +2593,17 @@ if (typeof window !== 'undefined') {
// Embedded modal method
embed: (container, options) => nostrLite.embed(container, options),
- // Floating tab methods
+ // CSS-only theme management API
+ switchTheme: (themeName) => nostrLite.switchTheme(themeName),
+ getCurrentTheme: () => nostrLite.getCurrentTheme(),
+ getAvailableThemes: () => nostrLite.getAvailableThemes(),
+
+ // Floating tab management API
showFloatingTab: () => nostrLite.showFloatingTab(),
hideFloatingTab: () => nostrLite.hideFloatingTab(),
+ toggleFloatingTab: () => nostrLite.toggleFloatingTab(),
updateFloatingTab: (options) => nostrLite.updateFloatingTab(options),
- destroyFloatingTab: () => nostrLite.destroyFloatingTab(),
+ getFloatingTabState: () => nostrLite.getFloatingTabState(),
// Expose for debugging
_extensionBridge: nostrLite.extensionBridge,
diff --git a/lite/ui/modal.js b/lite/ui/modal.js
index 8297c54..c208f6f 100644
--- a/lite/ui/modal.js
+++ b/lite/ui/modal.js
@@ -4,11 +4,13 @@
*/
class Modal {
- constructor(options) {
+ constructor(options = {}) {
this.options = options;
this.container = null;
this.isVisible = false;
this.currentScreen = null;
+ this.isEmbedded = !!options.embedded;
+ this.embeddedContainer = options.embedded;
// Initialize modal container and styles
this._initModal();
@@ -17,32 +19,59 @@ class Modal {
_initModal() {
// Create modal container
this.container = document.createElement('div');
- this.container.id = 'nl-modal';
- this.container.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.75);
- display: none;
- z-index: 10000;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- `;
+ this.container.id = this.isEmbedded ? 'nl-modal-embedded' : 'nl-modal';
+
+ if (this.isEmbedded) {
+ // Embedded mode: inline positioning, no overlay
+ this.container.style.cssText = `
+ position: relative;
+ display: none;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ width: 100%;
+ `;
+ } else {
+ // Modal mode: fixed overlay
+ this.container.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.75);
+ display: none;
+ z-index: 10000;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ `;
+ }
// Create modal content
const modalContent = document.createElement('div');
- modalContent.style.cssText = `
- position: relative;
- background: white;
- width: 90%;
- max-width: 400px;
- margin: 50px auto;
- border-radius: 12px;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
- max-height: 600px;
- overflow: hidden;
- `;
+ if (this.isEmbedded) {
+ // Embedded content: no centering margin, full width
+ modalContent.style.cssText = `
+ position: relative;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ width: 100%;
+ border-radius: var(--nl-border-radius, 15px);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ overflow: hidden;
+ `;
+ } else {
+ // Modal content: centered with margin
+ modalContent.style.cssText = `
+ position: relative;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ width: 90%;
+ max-width: 400px;
+ margin: 50px auto;
+ border-radius: var(--nl-border-radius, 15px);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ max-height: 600px;
+ overflow: hidden;
+ `;
+ }
// Header
const modalHeader = document.createElement('div');
@@ -51,6 +80,8 @@ class Modal {
display: flex;
justify-content: space-between;
align-items: center;
+ background: transparent;
+ border-bottom: none;
`;
const modalTitle = document.createElement('h2');
@@ -59,31 +90,44 @@ class Modal {
margin: 0;
font-size: 24px;
font-weight: 600;
- color: #1f2937;
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
- const closeButton = document.createElement('button');
- closeButton.innerHTML = 'ร';
- closeButton.onclick = () => this.close();
- closeButton.style.cssText = `
- background: none;
- border: none;
- font-size: 28px;
- color: #6b7280;
- cursor: pointer;
- padding: 0;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 6px;
- `;
- closeButton.onmouseover = () => closeButton.style.background = '#f3f4f6';
- closeButton.onmouseout = () => closeButton.style.background = 'none';
-
modalHeader.appendChild(modalTitle);
- modalHeader.appendChild(closeButton);
+
+ // Only add close button for non-embedded modals
+ // Embedded modals shouldn't have a close button because there's no way to reopen them
+ if (!this.isEmbedded) {
+ const closeButton = document.createElement('button');
+ closeButton.innerHTML = 'ร';
+ closeButton.onclick = () => this.close();
+ closeButton.style.cssText = `
+ background: var(--nl-secondary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
+ font-size: 28px;
+ color: var(--nl-primary-color);
+ cursor: pointer;
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ `;
+ closeButton.onmouseover = () => {
+ closeButton.style.borderColor = 'var(--nl-accent-color)';
+ closeButton.style.background = 'var(--nl-secondary-color)';
+ };
+ closeButton.onmouseout = () => {
+ closeButton.style.borderColor = 'var(--nl-primary-color)';
+ closeButton.style.background = 'var(--nl-secondary-color)';
+ };
+
+ modalHeader.appendChild(closeButton);
+ }
// Body
this.modalBody = document.createElement('div');
@@ -91,38 +135,52 @@ class Modal {
padding: 24px;
overflow-y: auto;
max-height: 500px;
+ background: transparent;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
modalContent.appendChild(modalHeader);
modalContent.appendChild(this.modalBody);
this.container.appendChild(modalContent);
- // Add to body
- document.body.appendChild(this.container);
-
- // Click outside to close
- this.container.onclick = (e) => {
- if (e.target === this.container) {
- this.close();
+ // Add to appropriate parent
+ if (this.isEmbedded && this.embeddedContainer) {
+ // Append to specified container for embedding
+ if (typeof this.embeddedContainer === 'string') {
+ const targetElement = document.querySelector(this.embeddedContainer);
+ if (targetElement) {
+ targetElement.appendChild(this.container);
+ } else {
+ console.error('NOSTR_LOGIN_LITE: Embedded container not found:', this.embeddedContainer);
+ document.body.appendChild(this.container);
+ }
+ } else if (this.embeddedContainer instanceof HTMLElement) {
+ this.embeddedContainer.appendChild(this.container);
+ } else {
+ console.error('NOSTR_LOGIN_LITE: Invalid embedded container');
+ document.body.appendChild(this.container);
}
- };
+ } else {
+ // Add to body for modal mode
+ document.body.appendChild(this.container);
+ }
+
+ // Click outside to close (only for modal mode)
+ if (!this.isEmbedded) {
+ this.container.onclick = (e) => {
+ if (e.target === this.container) {
+ this.close();
+ }
+ };
+ }
// Update theme
this.updateTheme();
}
updateTheme() {
- const isDark = this.options?.darkMode;
- const modalContent = this.container.querySelector(':nth-child(1)');
- const title = this.container.querySelector('h2');
-
- if (isDark) {
- modalContent.style.background = '#1f2937';
- title.style.color = 'white';
- } else {
- modalContent.style.background = 'white';
- title.style.color = '#1f2937';
- }
+ // The theme will automatically update through CSS custom properties
+ // No manual styling needed - the CSS variables handle everything
}
open(opts = {}) {
@@ -205,26 +263,41 @@ class Modal {
width: 100%;
padding: 16px;
margin-bottom: 12px;
- background: ${this.options?.darkMode ? '#374151' : 'white'};
- border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'};
- border-radius: 8px;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
cursor: pointer;
transition: all 0.2s;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
button.onmouseover = () => {
- button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
+ button.style.borderColor = 'var(--nl-accent-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
button.onmouseout = () => {
- button.style.boxShadow = 'none';
+ button.style.borderColor = 'var(--nl-primary-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
const iconDiv = document.createElement('div');
- iconDiv.textContent = option.icon;
+ // Replace emoji icons with text-based ones
+ const iconMap = {
+ '๐': '[EXT]',
+ '๐': '[KEY]',
+ '๐': '[NET]',
+ '๐๏ธ': '[VIEW]',
+ '๐ฑ': '[SMS]'
+ };
+ iconDiv.textContent = iconMap[option.icon] || option.icon;
iconDiv.style.cssText = `
- font-size: 24px;
+ font-size: 16px;
+ font-weight: bold;
margin-right: 16px;
- width: 24px;
+ width: 50px;
text-align: center;
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
const contentDiv = document.createElement('div');
@@ -235,14 +308,16 @@ class Modal {
titleDiv.style.cssText = `
font-weight: 600;
margin-bottom: 4px;
- color: ${this.options?.darkMode ? 'white' : '#1f2937'};
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
const descDiv = document.createElement('div');
descDiv.textContent = option.description;
descDiv.style.cssText = `
font-size: 14px;
- color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'};
+ color: #666666;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
contentDiv.appendChild(titleDiv);
@@ -446,11 +521,22 @@ class Modal {
const title = document.createElement('h3');
title.textContent = 'Choose Browser Extension';
- title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
+ title.style.cssText = `
+ margin: 0 0 16px 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ `;
const description = document.createElement('p');
description.textContent = `Found ${extensions.length} Nostr extensions. Choose which one to use:`;
- description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
+ description.style.cssText = `
+ margin-bottom: 20px;
+ color: #666666;
+ font-size: 14px;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
+ `;
this.modalBody.appendChild(title);
this.modalBody.appendChild(description);
@@ -465,21 +551,23 @@ class Modal {
width: 100%;
padding: 16px;
margin-bottom: 12px;
- background: ${this.options?.darkMode ? '#374151' : 'white'};
- border: 1px solid ${this.options?.darkMode ? '#4b5563' : '#d1d5db'};
- border-radius: 8px;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
cursor: pointer;
transition: all 0.2s;
text-align: left;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
button.onmouseover = () => {
- button.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
- button.style.transform = 'translateY(-1px)';
+ button.style.borderColor = 'var(--nl-accent-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
button.onmouseout = () => {
- button.style.boxShadow = 'none';
- button.style.transform = 'none';
+ button.style.borderColor = 'var(--nl-primary-color)';
+ button.style.background = 'var(--nl-secondary-color)';
};
const iconDiv = document.createElement('div');
@@ -499,15 +587,16 @@ class Modal {
nameDiv.style.cssText = `
font-weight: 600;
margin-bottom: 4px;
- color: ${this.options?.darkMode ? 'white' : '#1f2937'};
+ color: var(--nl-primary-color);
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
const pathDiv = document.createElement('div');
pathDiv.textContent = ext.name;
pathDiv.style.cssText = `
font-size: 12px;
- color: ${this.options?.darkMode ? '#9ca3af' : '#6b7280'};
- font-family: monospace;
+ color: #666666;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
contentDiv.appendChild(nameDiv);
@@ -1041,23 +1130,24 @@ class Modal {
display: block;
width: 100%;
padding: 12px;
- border: none;
- border-radius: 8px;
+ border: var(--nl-border-width) solid var(--nl-primary-color);
+ border-radius: var(--nl-border-radius);
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
+ font-family: var(--nl-font-family, 'Courier New', monospace);
`;
if (type === 'primary') {
return baseStyle + `
- background: #3b82f6;
- color: white;
+ background: var(--nl-secondary-color);
+ color: var(--nl-primary-color);
`;
} else {
return baseStyle + `
- background: #6b7280;
- color: white;
+ background: #cccccc;
+ color: var(--nl-primary-color);
`;
}
}