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); `; } }