added embedded option

This commit is contained in:
Your Name
2025-09-13 15:21:18 -04:00
parent 72b0d9b102
commit 3b1eb7f951
4 changed files with 871 additions and 81 deletions

View File

@@ -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