diff --git a/README.md b/README.md index 1d03f47..daba767 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,62 @@ Nostr_Login_Lite =========== + +## Floating Tab API + +Configure persistent floating tab for login/logout: + +```javascript +await NOSTR_LOGIN_LITE.init({ + floatingTab: { + enabled: true, + hPosition: 0.95, // 0.0-1.0 or '95%' from left + vPosition: 0.5, // 0.0-1.0 or '50%' from top + appearance: { + style: 'pill', // 'pill', 'square', 'circle', 'minimal' + theme: 'auto', // 'auto', 'light', 'dark' + icon: '🔐', + text: 'Login' + }, + behavior: { + hideWhenAuthenticated: false, + showUserInfo: true, + autoSlide: true + }, + animation: { + slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down' + } + } +}); +``` + +Control methods: +```javascript +NOSTR_LOGIN_LITE.showFloatingTab(); +NOSTR_LOGIN_LITE.hideFloatingTab(); +NOSTR_LOGIN_LITE.updateFloatingTab(options); +NOSTR_LOGIN_LITE.destroyFloatingTab(); +``` + +## Embedded Modal API + +Embed login interface directly into page element: + +```javascript +// Initialize library first +await NOSTR_LOGIN_LITE.init({ + methods: { + extension: true, + local: true, + readonly: true + } +}); + +// Embed into container +const modal = NOSTR_LOGIN_LITE.embed('#login-container', { + title: 'Login', + showHeader: true, + seamless: false // true = no borders/shadows, blends into page +}); +``` + +Container can be CSS selector or DOM element. Modal renders inline without backdrop overlay. diff --git a/examples/embedded.html b/examples/embedded.html new file mode 100644 index 0000000..f72df97 --- /dev/null +++ b/examples/embedded.html @@ -0,0 +1,56 @@ + + + + + + Embedded NOSTR_LOGIN_LITE + + + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/examples/login-and-profile.html b/examples/login-and-profile.html index fd5a47f..ac57226 100644 --- a/examples/login-and-profile.html +++ b/examples/login-and-profile.html @@ -271,9 +271,31 @@ extension: true, local: true, readonly: true, - remote: true, // Enables "Nostr Connect" (NIP-46) + connect: true, // Enables "Nostr Connect" (NIP-46) + remote: true, // Also needed for "Nostr Connect" compatibility otp: true // Enables "DM/OTP" }, + floatingTab: { + enabled: true, + hPosition: 0.80, // 95% from left + vPosition: 0.01, // 50% from top (center) + appearance: { + style: 'minimal', + theme: 'auto', + icon: '', + text: 'Login', + iconOnly: false + }, + behavior: { + hideWhenAuthenticated: false, + showUserInfo: true, + autoSlide: false, + persistent: false + }, + animation: { + slideDirection: 'right' // Slide to the right when hiding + } + }, debug: true }); diff --git a/lite/nostr-lite.js b/lite/nostr-lite.js index d5025dc..fbe805c 100644 --- a/lite/nostr-lite.js +++ b/lite/nostr-lite.js @@ -546,107 +546,179 @@ class Modal { this.container = null; this.isVisible = false; this.currentScreen = null; + this.floatingTab = null; + this.isEmbedded = false; + this.embedContainer = null; // 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 = '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-embedded-modal' : 'nl-modal'; + + if (this.isEmbedded) { + // Embedded mode styles + this.container.style.cssText = ` + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + width: 100%; + `; + } else { + // Floating mode styles + 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; + `; + } // 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 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 + 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; + `; + } - // Header - const modalHeader = document.createElement('div'); - modalHeader.style.cssText = ` - padding: 20px 24px 0 24px; - display: flex; - justify-content: space-between; - align-items: center; - `; + // 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; + display: flex; + justify-content: space-between; + align-items: center; + `; - const modalTitle = document.createElement('h2'); - modalTitle.textContent = 'Nostr Login'; - modalTitle.style.cssText = ` - margin: 0; - font-size: 24px; - font-weight: 600; - color: #1f2937; - `; + 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; + `; - 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(modalTitle); - modalHeader.appendChild(closeButton); + // 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); + } // Body this.modalBody = document.createElement('div'); this.modalBody.style.cssText = ` padding: 24px; overflow-y: auto; - max-height: 500px; + ${this.isEmbedded ? '' : 'max-height: 500px;'} `; - 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 container + if (this.isEmbedded) { + this.embedContainer.appendChild(this.container); + } else { + document.body.appendChild(this.container); + + // Click outside to close (floating mode only) + this.container.onclick = (e) => { + if (e.target === this.container) { + this.close(); + } + }; + } // 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; @@ -665,7 +737,12 @@ class Modal { open(opts = {}) { this.currentScreen = opts.startScreen; this.isVisible = true; - this.container.style.display = 'block'; + + if (this.isEmbedded) { + this.container.style.display = 'block'; + } else { + this.container.style.display = 'block'; + } // Render login options this._renderLoginOptions(); @@ -673,8 +750,14 @@ class Modal { close() { this.isVisible = false; - this.container.style.display = 'none'; - this.modalBody.innerHTML = ''; + + if (this.isEmbedded) { + // For embedded mode, just clear content but keep visible + this.modalBody.innerHTML = ''; + } else { + this.container.style.display = 'none'; + this.modalBody.innerHTML = ''; + } } _renderLoginOptions() { @@ -2007,6 +2090,32 @@ 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 @@ -2017,6 +2126,456 @@ window.addEventListener('load', () => { }); +// ====================================== +// Floating Tab Component +// ====================================== + +class FloatingTab { + constructor(modal, options = {}) { + 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%' + offset: { x: 0, y: 0 }, + appearance: { + style: 'pill', + theme: 'auto', + icon: '🔐', + text: 'Login', + iconOnly: false + }, + behavior: { + hideWhenAuthenticated: true, + 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 + }; + + this.container = null; + this.isVisible = false; + this.isAuthenticated = false; + this.userInfo = null; + + this._init(); + } + + _init() { + this._createContainer(); + this._attachEventListeners(); + this._updateAppearance(); + } + + _createContainer() { + this.container = document.createElement('div'); + this.container.className = 'nl-floating-tab'; + this.container.id = '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 += ` + position: fixed; + z-index: 9998; + 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); + `; + + // 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; + } + + _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() { + // 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', {}); + } + }); + + // 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 + if (this.options.behavior.autoSlide) { + setTimeout(() => this._updateSlideState(false), 100); + } + } + } + + 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; + + if (isAuthenticated && this.options.behavior.hideWhenAuthenticated) { + this.hide(); + } else { + this._updateAppearance(); + if (!this.isVisible) { + this.show(); + } + } + } + + destroy() { + if (this.container) { + this.container.remove(); + this.container = null; + } + this.isVisible = false; + } +} + // ====================================== // Main NOSTR_LOGIN_LITE Library // ====================================== @@ -2080,7 +2639,7 @@ class NostrLite { async init(options = {}) { console.log('NOSTR_LOGIN_LITE: Initializing with options:', options); - this.options = { + this.options = this._deepMerge({ theme: 'light', darkMode: false, relays: ['wss://relay.damus.io', 'wss://nos.lol'], @@ -2091,8 +2650,32 @@ class NostrLite { connect: false, otp: false }, - ...options - }; + floatingTab: { + enabled: false, + hPosition: 1.0, // 100% from left (right edge) + vPosition: 0.5, // 50% from top (center) + offset: { x: 0, y: 0 }, + appearance: { + style: 'pill', + theme: 'auto', + icon: '🔐', + text: 'Login', + iconOnly: false + }, + behavior: { + hideWhenAuthenticated: true, + 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); // Set up window.nostr facade if no extension detected if (this.extensionBridge.getExtensionCount() === 0) { @@ -2105,9 +2688,28 @@ class NostrLite { // 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); @@ -2289,9 +2891,11 @@ 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 if (typeof Modal !== 'undefined') { + this.modal = new Modal(this.options); + this.modal.open({ startScreen }); } else { console.error('NOSTR_LOGIN_LITE: Modal component not available'); } @@ -2312,6 +2916,45 @@ class NostrLite { })); } } + + // Floating tab methods + showFloatingTab() { + if (this.modal) { + this.modal.showFloatingTab(); + } + } + + hideFloatingTab() { + if (this.modal) { + this.modal.hideFloatingTab(); + } + } + + updateFloatingTab(options) { + if (this.modal) { + this.modal.updateFloatingTab(options); + } + } + + destroyFloatingTab() { + if (this.modal) { + this.modal.destroyFloatingTab(); + } + } + + embed(container, options = {}) { + const embedOptions = { + ...this.options, + ...options, + embedded: container + }; + + // Create new modal instance for embedding + const embeddedModal = new Modal(embedOptions); + embeddedModal.open(); + + return embeddedModal; + } } // Window.nostr facade for when no extension is available @@ -2365,6 +3008,15 @@ if (typeof window !== 'undefined') { launch: (startScreen) => nostrLite.launch(startScreen), logout: () => nostrLite.logout(), + // Embedded modal method + embed: (container, options) => nostrLite.embed(container, options), + + // Floating tab methods + showFloatingTab: () => nostrLite.showFloatingTab(), + hideFloatingTab: () => nostrLite.hideFloatingTab(), + updateFloatingTab: (options) => nostrLite.updateFloatingTab(options), + destroyFloatingTab: () => nostrLite.destroyFloatingTab(), + // Expose for debugging _extensionBridge: nostrLite.extensionBridge, _instance: nostrLite