added embedded option
This commit is contained in:
60
README.md
60
README.md
@@ -1,2 +1,62 @@
|
|||||||
Nostr_Login_Lite
|
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.
|
||||||
|
|||||||
56
examples/embedded.html
Normal file
56
examples/embedded.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Embedded NOSTR_LOGIN_LITE</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-container {
|
||||||
|
/* No styling - let embedded modal blend seamlessly */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div id="login-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../lite/nostr.bundle.js"></script>
|
||||||
|
<script src="../lite/nostr-lite.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
await window.NOSTR_LOGIN_LITE.init({
|
||||||
|
methods: {
|
||||||
|
extension: true,
|
||||||
|
local: true,
|
||||||
|
readonly: true,
|
||||||
|
connect: true,
|
||||||
|
remote: true,
|
||||||
|
otp: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.NOSTR_LOGIN_LITE.embed('#login-container', {
|
||||||
|
seamless: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -271,9 +271,31 @@
|
|||||||
extension: true,
|
extension: true,
|
||||||
local: true,
|
local: true,
|
||||||
readonly: 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"
|
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
|
debug: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -546,107 +546,179 @@ class Modal {
|
|||||||
this.container = null;
|
this.container = null;
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
this.currentScreen = null;
|
this.currentScreen = null;
|
||||||
|
this.floatingTab = null;
|
||||||
|
this.isEmbedded = false;
|
||||||
|
this.embedContainer = null;
|
||||||
|
|
||||||
// Initialize modal container and styles
|
// Initialize modal container and styles
|
||||||
this._initModal();
|
this._initModal();
|
||||||
|
|
||||||
|
// Initialize floating tab if enabled (only for floating modals)
|
||||||
|
if (this.options?.floatingTab?.enabled && !this.isEmbedded) {
|
||||||
|
this._initFloatingTab();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initModal() {
|
_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
|
// Create modal container
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement('div');
|
||||||
this.container.id = 'nl-modal';
|
this.container.id = this.isEmbedded ? 'nl-embedded-modal' : 'nl-modal';
|
||||||
this.container.style.cssText = `
|
|
||||||
position: fixed;
|
if (this.isEmbedded) {
|
||||||
top: 0;
|
// Embedded mode styles
|
||||||
left: 0;
|
this.container.style.cssText = `
|
||||||
right: 0;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
bottom: 0;
|
width: 100%;
|
||||||
background: rgba(0, 0, 0, 0.75);
|
`;
|
||||||
display: none;
|
} else {
|
||||||
z-index: 10000;
|
// Floating mode styles
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
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
|
// Create modal content
|
||||||
const modalContent = document.createElement('div');
|
const modalContent = document.createElement('div');
|
||||||
modalContent.style.cssText = `
|
if (this.isEmbedded) {
|
||||||
position: relative;
|
// Embedded content styles
|
||||||
background: white;
|
if (this.options?.seamless) {
|
||||||
width: 90%;
|
// Seamless mode - no borders, shadows, or background
|
||||||
max-width: 400px;
|
modalContent.style.cssText = `
|
||||||
margin: 50px auto;
|
background: transparent;
|
||||||
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);
|
} else {
|
||||||
max-height: 600px;
|
// Standard embedded mode
|
||||||
overflow: hidden;
|
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
|
// Header (optional for embedded mode)
|
||||||
const modalHeader = document.createElement('div');
|
if (!this.isEmbedded || this.options?.showHeader !== false) {
|
||||||
modalHeader.style.cssText = `
|
const modalHeader = document.createElement('div');
|
||||||
padding: 20px 24px 0 24px;
|
modalHeader.style.cssText = `
|
||||||
display: flex;
|
padding: 20px 24px 0 24px;
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
`;
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
const modalTitle = document.createElement('h2');
|
const modalTitle = document.createElement('h2');
|
||||||
modalTitle.textContent = 'Nostr Login';
|
modalTitle.textContent = this.options?.title || 'Nostr Login';
|
||||||
modalTitle.style.cssText = `
|
modalTitle.style.cssText = `
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const closeButton = document.createElement('button');
|
modalHeader.appendChild(modalTitle);
|
||||||
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);
|
// Close button (only for floating modals)
|
||||||
modalHeader.appendChild(closeButton);
|
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
|
// Body
|
||||||
this.modalBody = document.createElement('div');
|
this.modalBody = document.createElement('div');
|
||||||
this.modalBody.style.cssText = `
|
this.modalBody.style.cssText = `
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 500px;
|
${this.isEmbedded ? '' : 'max-height: 500px;'}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
modalContent.appendChild(modalHeader);
|
|
||||||
modalContent.appendChild(this.modalBody);
|
modalContent.appendChild(this.modalBody);
|
||||||
this.container.appendChild(modalContent);
|
this.container.appendChild(modalContent);
|
||||||
|
|
||||||
// Add to body
|
// Add to appropriate container
|
||||||
document.body.appendChild(this.container);
|
if (this.isEmbedded) {
|
||||||
|
this.embedContainer.appendChild(this.container);
|
||||||
// Click outside to close
|
} else {
|
||||||
this.container.onclick = (e) => {
|
document.body.appendChild(this.container);
|
||||||
if (e.target === this.container) {
|
|
||||||
this.close();
|
// Click outside to close (floating mode only)
|
||||||
}
|
this.container.onclick = (e) => {
|
||||||
};
|
if (e.target === this.container) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Update theme
|
// Update theme
|
||||||
this.updateTheme();
|
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() {
|
updateTheme() {
|
||||||
const isDark = this.options?.darkMode;
|
const isDark = this.options?.darkMode;
|
||||||
@@ -665,7 +737,12 @@ class Modal {
|
|||||||
open(opts = {}) {
|
open(opts = {}) {
|
||||||
this.currentScreen = opts.startScreen;
|
this.currentScreen = opts.startScreen;
|
||||||
this.isVisible = true;
|
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
|
// Render login options
|
||||||
this._renderLoginOptions();
|
this._renderLoginOptions();
|
||||||
@@ -673,8 +750,14 @@ class Modal {
|
|||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.isVisible = false;
|
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() {
|
_renderLoginOptions() {
|
||||||
@@ -2007,6 +2090,32 @@ class Modal {
|
|||||||
static getInstance() {
|
static getInstance() {
|
||||||
return Modal.instance;
|
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
|
// 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
|
// Main NOSTR_LOGIN_LITE Library
|
||||||
// ======================================
|
// ======================================
|
||||||
@@ -2080,7 +2639,7 @@ class NostrLite {
|
|||||||
async init(options = {}) {
|
async init(options = {}) {
|
||||||
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
|
console.log('NOSTR_LOGIN_LITE: Initializing with options:', options);
|
||||||
|
|
||||||
this.options = {
|
this.options = this._deepMerge({
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||||
@@ -2091,8 +2650,32 @@ class NostrLite {
|
|||||||
connect: false,
|
connect: false,
|
||||||
otp: 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
|
// Set up window.nostr facade if no extension detected
|
||||||
if (this.extensionBridge.getExtensionCount() === 0) {
|
if (this.extensionBridge.getExtensionCount() === 0) {
|
||||||
@@ -2105,9 +2688,28 @@ class NostrLite {
|
|||||||
// Set up event listeners for authentication flow
|
// Set up event listeners for authentication flow
|
||||||
this._setupAuthEventHandlers();
|
this._setupAuthEventHandlers();
|
||||||
|
|
||||||
|
// Initialize modal with floating tab support
|
||||||
|
this.modal = new Modal(this.options);
|
||||||
|
|
||||||
return this;
|
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() {
|
_setupWindowNostrFacade() {
|
||||||
if (typeof window !== 'undefined' && !window.nostr) {
|
if (typeof window !== 'undefined' && !window.nostr) {
|
||||||
window.nostr = new WindowNostr(this);
|
window.nostr = new WindowNostr(this);
|
||||||
@@ -2289,9 +2891,11 @@ class NostrLite {
|
|||||||
launch(startScreen = 'login') {
|
launch(startScreen = 'login') {
|
||||||
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen);
|
||||||
|
|
||||||
if (typeof Modal !== 'undefined') {
|
if (this.modal) {
|
||||||
const modal = new Modal(this.options);
|
this.modal.open({ startScreen });
|
||||||
modal.open({ startScreen });
|
} else if (typeof Modal !== 'undefined') {
|
||||||
|
this.modal = new Modal(this.options);
|
||||||
|
this.modal.open({ startScreen });
|
||||||
} else {
|
} else {
|
||||||
console.error('NOSTR_LOGIN_LITE: Modal component not available');
|
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
|
// Window.nostr facade for when no extension is available
|
||||||
@@ -2365,6 +3008,15 @@ if (typeof window !== 'undefined') {
|
|||||||
launch: (startScreen) => nostrLite.launch(startScreen),
|
launch: (startScreen) => nostrLite.launch(startScreen),
|
||||||
logout: () => nostrLite.logout(),
|
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
|
// Expose for debugging
|
||||||
_extensionBridge: nostrLite.extensionBridge,
|
_extensionBridge: nostrLite.extensionBridge,
|
||||||
_instance: nostrLite
|
_instance: nostrLite
|
||||||
|
|||||||
Reference in New Issue
Block a user