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