diff --git a/go.mod b/go.mod
index f008af6..69c7110 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,9 @@ require (
)
require (
+ github.com/coreos/go-oidc/v3 v3.15.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
@@ -30,7 +32,9 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
+ golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+ golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index d064efc..6fe388b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
+github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -11,6 +13,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
@@ -71,10 +75,14 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
+golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
diff --git a/internal/content/assets/insertr.css b/internal/content/assets/insertr.css
index 499b116..cc07563 100644
--- a/internal/content/assets/insertr.css
+++ b/internal/content/assets/insertr.css
@@ -85,22 +85,85 @@
}
/* =================================================================
- AUTHENTICATION CONTROLS
+ UNIFIED CONTROL PANEL
================================================================= */
-.insertr-auth-controls {
+.insertr-control-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: var(--insertr-z-overlay);
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: var(--insertr-spacing-sm);
font-family: var(--insertr-font-family);
font-size: var(--insertr-font-size-base);
+ max-width: 280px;
}
-.insertr-auth-btn {
+/* Status Section */
+.insertr-status-section {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: var(--insertr-spacing-xs);
+}
+
+.insertr-status-indicator {
+ background: var(--insertr-bg-primary);
+ color: var(--insertr-text-primary);
+ border: 1px solid var(--insertr-border-color);
+ border-radius: var(--insertr-border-radius);
+ padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--insertr-spacing-xs);
+ font-size: var(--insertr-font-size-sm);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: var(--insertr-transition);
+}
+
+.insertr-status-text {
+ margin: 0;
+ padding: 0;
+ font-weight: 500;
+ color: inherit;
+ white-space: nowrap;
+}
+
+.insertr-status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ transition: var(--insertr-transition);
+}
+
+.insertr-status-visitor {
+ background: var(--insertr-text-muted);
+}
+
+.insertr-status-authenticated {
+ background: var(--insertr-primary);
+}
+
+.insertr-status-editing {
+ background: var(--insertr-success);
+ animation: insertr-pulse 2s infinite;
+}
+
+@keyframes insertr-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+/* Action Buttons Section */
+.insertr-action-section {
+ display: flex;
+ flex-direction: column;
+ gap: var(--insertr-spacing-xs);
+}
+
+.insertr-action-btn {
background: var(--insertr-primary);
color: var(--insertr-text-inverse);
border: none;
@@ -117,21 +180,112 @@
cursor: pointer;
transition: var(--insertr-transition);
line-height: var(--insertr-line-height);
- min-width: 80px;
+ min-width: 120px;
white-space: nowrap;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
-.insertr-auth-btn:hover {
+.insertr-action-btn:hover {
background: var(--insertr-primary-hover);
color: var(--insertr-text-inverse);
text-decoration: none;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
-.insertr-auth-btn:focus {
+.insertr-action-btn:focus {
outline: 2px solid var(--insertr-primary);
outline-offset: 2px;
}
+.insertr-action-btn:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.insertr-action-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* Button Type Variants */
+.insertr-enhance-btn {
+ background: var(--insertr-info);
+}
+
+.insertr-enhance-btn:hover {
+ background: #138496;
+}
+
+.insertr-edit-btn.insertr-edit-active {
+ background: var(--insertr-success);
+}
+
+.insertr-edit-btn.insertr-edit-active:hover {
+ background: #1e7e34;
+}
+
+.insertr-auth-btn.insertr-authenticated {
+ background: var(--insertr-text-secondary);
+}
+
+.insertr-auth-btn.insertr-authenticated:hover {
+ background: var(--insertr-text-primary);
+}
+
+/* =================================================================
+ EDITING INDICATORS
+ Visual feedback for editable content
+ ================================================================= */
+
+.insertr-editing-hover {
+ outline: 2px dashed var(--insertr-primary);
+ outline-offset: 2px;
+ background: rgba(0, 123, 255, 0.05);
+ position: relative;
+ transition: var(--insertr-transition);
+}
+
+.insertr-editing-hover::after {
+ content: '✏️ Click to edit';
+ position: absolute;
+ top: -30px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--insertr-text-primary);
+ color: var(--insertr-text-inverse);
+ padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
+ border-radius: var(--insertr-border-radius);
+ font-size: var(--insertr-font-size-sm);
+ font-family: var(--insertr-font-family);
+ white-space: nowrap;
+ z-index: var(--insertr-z-tooltip);
+ opacity: 0;
+ animation: insertr-tooltip-show 0.2s ease-in-out forwards;
+}
+
+@keyframes insertr-tooltip-show {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(-5px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+/* Hide editing indicators when not in edit mode */
+body:not(.insertr-edit-mode) .insertr-editing-hover {
+ outline: none;
+ background: transparent;
+}
+
+body:not(.insertr-edit-mode) .insertr-editing-hover::after {
+ display: none;
+}
+
/* =================================================================
MODAL OVERLAY & CONTAINER
================================================================= */
@@ -517,9 +671,27 @@
padding: var(--insertr-spacing-sm);
}
- .insertr-auth-controls {
+ .insertr-control-panel {
bottom: 10px;
right: 10px;
+ max-width: 240px;
+ }
+
+ .insertr-action-btn {
+ min-width: 100px;
+ font-size: var(--insertr-font-size-sm);
+ padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
+ }
+
+ .insertr-status-indicator {
+ font-size: 11px;
+ padding: 3px var(--insertr-spacing-xs);
+ }
+
+ .insertr-editing-hover::after {
+ font-size: 11px;
+ padding: 2px var(--insertr-spacing-xs);
+ top: -25px;
}
.insertr-form-actions {
diff --git a/lib/src/core/auth.js b/lib/src/core/auth.js
index 237ae0a..31a4873 100644
--- a/lib/src/core/auth.js
+++ b/lib/src/core/auth.js
@@ -1,6 +1,12 @@
/**
* InsertrAuth - Authentication and state management
- * Handles user authentication, edit mode, and visual state changes
+ * Pure business logic - no DOM manipulation or UI concerns
+ *
+ * Responsibilities:
+ * - Manage authentication state
+ * - Handle OAuth flows
+ * - Validate permissions
+ * - Emit state change events
*/
export class InsertrAuth {
constructor(options = {}) {
@@ -20,7 +26,33 @@ export class InsertrAuth {
isAuthenticating: false
};
- this.statusIndicator = null;
+ // Event listeners for state changes
+ this.listeners = {
+ stateChange: [],
+ authChange: [],
+ editModeChange: []
+ };
+ }
+
+ /**
+ * Event emitter methods
+ */
+ on(event, callback) {
+ if (this.listeners[event]) {
+ this.listeners[event].push(callback);
+ }
+ }
+
+ off(event, callback) {
+ if (this.listeners[event]) {
+ this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
+ }
+ }
+
+ emit(event, data) {
+ if (this.listeners[event]) {
+ this.listeners[event].forEach(callback => callback(data));
+ }
}
/**
@@ -28,7 +60,6 @@ export class InsertrAuth {
*/
init() {
console.log('🔧 Insertr: Scanning for editor gates');
-
this.setupEditorGates();
}
@@ -42,11 +73,6 @@ export class InsertrAuth {
console.log('🔐 Initializing Insertr Editing System');
- this.createAuthControls();
- this.setupAuthenticationControls();
- this.createStatusIndicator();
- this.updateBodyClasses();
-
// Auto-enable edit mode after OAuth
this.state.editMode = true;
this.state.isInitialized = true;
@@ -56,10 +82,10 @@ export class InsertrAuth {
window.Insertr.startEditor();
}
- this.updateButtonStates();
- this.updateStatusIndicator();
+ // Emit state change for UI to update
+ this.emitStateChange();
- console.log('📱 Editing system active - Controls in bottom-right corner');
+ console.log('📱 Editing system active');
console.log('✏️ Edit mode enabled - Click elements to edit');
}
@@ -76,9 +102,6 @@ export class InsertrAuth {
console.log(`🚪 Found ${gates.length} editor gate(s)`);
- // Add gate styles
- this.addGateStyles();
-
gates.forEach((gate, index) => {
// Store original text for later restoration
if (!gate.hasAttribute('data-original-text')) {
@@ -90,7 +113,7 @@ export class InsertrAuth {
this.handleGateClick(gate, index);
});
- // Add subtle styling to indicate it's clickable
+ // Add minimal styling to indicate it's clickable
gate.style.cursor = 'pointer';
});
}
@@ -121,7 +144,7 @@ export class InsertrAuth {
// Initialize full editing system
this.initializeFullSystem();
- // Conditionally hide gates based on options
+ // Handle gate visibility based on options
if (this.options.hideGatesAfterAuth) {
this.hideAllGates();
} else {
@@ -198,47 +221,6 @@ export class InsertrAuth {
console.log('🚪 Editor gates restored to original state');
}
- /**
- * Create authentication control buttons (bottom-right positioned)
- */
- createAuthControls() {
- // Check if controls already exist
- if (document.getElementById('insertr-auth-controls')) {
- return;
- }
-
- const controlsHtml = `
-
-
-
-
- `;
-
- // Add controls to page
- document.body.insertAdjacentHTML('beforeend', controlsHtml);
-
- // Add styles for controls
-
- }
-
- /**
- * Setup event listeners for authentication controls
- */
- setupAuthenticationControls() {
- const authToggle = document.getElementById('insertr-auth-toggle');
- const editToggle = document.getElementById('insertr-edit-toggle');
-
- if (authToggle) {
- authToggle.addEventListener('click', () => this.toggleAuthentication());
- }
-
- if (editToggle) {
- editToggle.addEventListener('click', () => this.toggleEditMode());
- }
-
-
- }
-
/**
* Toggle authentication state
*/
@@ -255,9 +237,7 @@ export class InsertrAuth {
this.state.editMode = false;
}
- this.updateBodyClasses();
- this.updateButtonStates();
- this.updateStatusIndicator();
+ this.emitStateChange();
console.log(this.state.isAuthenticated
? '✅ Authenticated as Demo User'
@@ -281,9 +261,7 @@ export class InsertrAuth {
this.state.activeEditor = null;
}
- this.updateBodyClasses();
- this.updateButtonStates();
- this.updateStatusIndicator();
+ this.emitStateChange();
console.log(this.state.editMode
? '✏️ Edit mode ON - Click elements to edit'
@@ -291,83 +269,18 @@ export class InsertrAuth {
}
/**
- * Update body CSS classes based on authentication state
+ * Emit state change events for UI updates
*/
- updateBodyClasses() {
- document.body.classList.toggle('insertr-authenticated', this.state.isAuthenticated);
- document.body.classList.toggle('insertr-edit-mode', this.state.editMode);
- }
-
- /**
- * Update button text and visibility
- */
- updateButtonStates() {
- const authBtn = document.getElementById('insertr-auth-toggle');
- const editBtn = document.getElementById('insertr-edit-toggle');
-
- if (authBtn) {
- authBtn.textContent = this.state.isAuthenticated ? 'Logout' : 'Login as Client';
- authBtn.className = `insertr-auth-btn ${this.state.isAuthenticated ? 'insertr-authenticated' : ''}`;
- }
-
- if (editBtn) {
- editBtn.style.display = this.state.isAuthenticated ? 'inline-block' : 'none';
- editBtn.textContent = `Edit Mode: ${this.state.editMode ? 'On' : 'Off'}`;
- editBtn.className = `insertr-auth-btn ${this.state.editMode ? 'insertr-edit-active' : ''}`;
- }
-
- // Update enhance button visibility
- this.updateEnhanceButtonVisibility();
- }
-
- /**
- * Create status indicator
- */
- createStatusIndicator() {
- // Check if already exists
- if (document.getElementById('insertr-status')) {
- return;
- }
-
- const statusHtml = `
-
- `;
-
- document.body.insertAdjacentHTML('beforeend', statusHtml);
- this.statusIndicator = document.getElementById('insertr-status');
- this.setupEnhanceButton();
- this.updateStatusIndicator();
- }
-
- /**
- * Update status indicator text and style
- */
- updateStatusIndicator() {
- const statusText = document.querySelector('.insertr-status-text');
- const statusDot = document.querySelector('.insertr-status-dot');
-
- if (!statusText || !statusDot) return;
-
- if (!this.state.isAuthenticated) {
- statusText.textContent = 'Visitor Mode';
- statusDot.className = 'insertr-status-dot insertr-status-visitor';
- } else if (this.state.editMode) {
- statusText.textContent = 'Editing';
- statusDot.className = 'insertr-status-dot insertr-status-editing';
- } else {
- statusText.textContent = 'Authenticated';
- statusDot.className = 'insertr-status-dot insertr-status-authenticated';
- }
-
+ emitStateChange() {
+ const stateData = {
+ isAuthenticated: this.state.isAuthenticated,
+ editMode: this.state.editMode,
+ currentUser: this.state.currentUser
+ };
+ this.emit('stateChange', stateData);
+ this.emit('authChange', { isAuthenticated: this.state.isAuthenticated, user: this.state.currentUser });
+ this.emit('editModeChange', { editMode: this.state.editMode });
}
/**
@@ -391,34 +304,6 @@ export class InsertrAuth {
return this.state.currentUser;
}
- /**
- * Add minimal styles for editor gates
- */
- addGateStyles() {
- const styles = `
- .insertr-gate {
- transition: opacity 0.2s ease;
- user-select: none;
- }
-
- .insertr-gate:hover {
- opacity: 0.7;
- }
-
- /* Optional: Hide gates when authenticated (only if hideGatesAfterAuth option is true) */
- body.insertr-hide-gates .insertr-gate {
- display: none !important;
- }
- `;
-
- const styleSheet = document.createElement('style');
- styleSheet.type = 'text/css';
- styleSheet.innerHTML = styles;
- document.head.appendChild(styleSheet);
- }
-
-
-
/**
* OAuth integration placeholder
* In production, this would handle real OAuth flows
@@ -437,99 +322,44 @@ export class InsertrAuth {
role: 'editor'
};
- this.updateBodyClasses();
- this.updateButtonStates();
- this.updateStatusIndicator();
+ this.emitStateChange();
console.log('✅ OAuth authentication successful');
}, 1000);
}
/**
- * Setup enhance button functionality
+ * Validate if user has permission for specific action
*/
- setupEnhanceButton() {
- const enhanceBtn = document.getElementById('insertr-enhance-btn');
- if (!enhanceBtn) return;
+ hasPermission(action) {
+ if (!this.isAuthenticated()) return false;
+
+ const user = this.getCurrentUser();
+ if (!user) return false;
- enhanceBtn.addEventListener('click', async () => {
- await this.enhanceFiles();
- });
-
- // Show enhance button only in development/authenticated mode
- this.updateEnhanceButtonVisibility();
- }
-
- /**
- * Update enhance button visibility based on authentication state
- */
- updateEnhanceButtonVisibility() {
- const enhanceBtn = document.getElementById('insertr-enhance-btn');
- if (!enhanceBtn) return;
-
- // Show enhance button when authenticated (indicates dev mode)
- if (this.state.isAuthenticated) {
- enhanceBtn.style.display = 'inline-block';
- } else {
- enhanceBtn.style.display = 'none';
+ // Simple role-based permissions
+ switch (action) {
+ case 'edit':
+ return ['admin', 'editor'].includes(user.role);
+ case 'enhance':
+ return ['admin'].includes(user.role);
+ case 'manage':
+ return user.role === 'admin';
+ default:
+ return false;
}
}
/**
- * Trigger manual file enhancement
+ * Get authentication state snapshot
*/
- async enhanceFiles() {
- const enhanceBtn = document.getElementById('insertr-enhance-btn');
- if (!enhanceBtn) return;
-
- // Get site ID from window context or configuration
- const siteId = window.insertrConfig?.siteId || this.options.siteId || 'demo';
-
- try {
- // Show loading state
- enhanceBtn.textContent = '⏳ Enhancing...';
- enhanceBtn.disabled = true;
-
- // Smart server detection for enhance API (same logic as ApiClient)
- const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
- const enhanceUrl = isDevelopment
- ? `http://localhost:8080/api/enhance?site_id=${siteId}` // Development: separate API server
- : `/api/enhance?site_id=${siteId}`; // Production: same-origin API
-
- // Call enhance API
- const response = await fetch(enhanceUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${this.state.currentUser?.token || 'mock-token'}`
- }
- });
-
- if (!response.ok) {
- throw new Error(`Enhancement failed: ${response.status} ${response.statusText}`);
- }
-
- const result = await response.json();
- console.log('✅ Files enhanced successfully:', result);
-
- // Show success state briefly
- enhanceBtn.textContent = '✅ Enhanced!';
-
- // Reset button after success (no page reload needed)
- setTimeout(() => {
- enhanceBtn.textContent = '🔄 Enhance';
- enhanceBtn.disabled = false;
- }, 2000);
-
- } catch (error) {
- console.error('❌ Enhancement failed:', error);
- enhanceBtn.textContent = '❌ Failed';
-
- // Reset button after error
- setTimeout(() => {
- enhanceBtn.textContent = '🔄 Enhance';
- enhanceBtn.disabled = false;
- }, 2000);
- }
+ getState() {
+ return {
+ isAuthenticated: this.state.isAuthenticated,
+ editMode: this.state.editMode,
+ currentUser: this.state.currentUser,
+ isInitialized: this.state.isInitialized,
+ isAuthenticating: this.state.isAuthenticating
+ };
}
}
\ No newline at end of file
diff --git a/lib/src/core/editor.js b/lib/src/core/editor.js
index 76a96f0..fda7bed 100644
--- a/lib/src/core/editor.js
+++ b/lib/src/core/editor.js
@@ -1,7 +1,14 @@
import { InsertrFormRenderer } from '../ui/form-renderer.js';
/**
- * InsertrEditor - Visual editing functionality
+ * InsertrEditor - Content editing workflow and business logic
+ * Pure business logic - no visual styling or DOM manipulation
+ *
+ * Responsibilities:
+ * - Manage editing workflow
+ * - Handle content persistence
+ * - Coordinate with form renderer
+ * - Emit editing events
*/
export class InsertrEditor {
constructor(core, auth, apiClient, options = {}) {
@@ -11,6 +18,34 @@ export class InsertrEditor {
this.options = options;
this.isActive = false;
this.formRenderer = new InsertrFormRenderer(apiClient);
+
+ // Event listeners for mode changes
+ this.listeners = {
+ modeChange: [],
+ editStart: [],
+ editEnd: []
+ };
+ }
+
+ /**
+ * Event emitter methods
+ */
+ on(event, callback) {
+ if (this.listeners[event]) {
+ this.listeners[event].push(callback);
+ }
+ }
+
+ off(event, callback) {
+ if (this.listeners[event]) {
+ this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
+ }
+ }
+
+ emit(event, data) {
+ if (this.listeners[event]) {
+ this.listeners[event].forEach(callback => callback(data));
+ }
}
start() {
@@ -19,37 +54,23 @@ export class InsertrEditor {
console.log('🚀 Starting Insertr Editor');
this.isActive = true;
-
-
- // Initialize all enhanced elements
+ // Initialize all enhanced elements for editing
const elements = this.core.getAllElements();
console.log(`📝 Found ${elements.length} editable elements`);
elements.forEach(meta => this.initializeElement(meta));
+
+ // Emit mode change event for UI updates
+ this.emit('modeChange', { isActive: this.isActive });
}
initializeElement(meta) {
- const { element, contentId, contentType } = meta;
+ const { element } = meta;
- // Add visual indicators
- element.style.cursor = 'pointer';
- element.style.position = 'relative';
-
- // Add interaction handlers
- this.addHoverEffects(element);
+ // Add click handler for editing
this.addClickHandler(element, meta);
}
- addHoverEffects(element) {
- element.addEventListener('mouseenter', () => {
- element.classList.add('insertr-editing-hover');
- });
-
- element.addEventListener('mouseleave', () => {
- element.classList.remove('insertr-editing-hover');
- });
- }
-
addClickHandler(element, meta) {
element.addEventListener('click', (e) => {
// Only allow editing if authenticated and in edit mode
diff --git a/lib/src/index.js b/lib/src/index.js
index d40c78b..484ac65 100644
--- a/lib/src/index.js
+++ b/lib/src/index.js
@@ -7,14 +7,18 @@ import { InsertrCore } from './core/insertr.js';
import { InsertrEditor } from './core/editor.js';
import { InsertrAuth } from './core/auth.js';
import { ApiClient } from './core/api-client.js';
+import { InsertrControlPanel } from './ui/control-panel.js';
// Create global Insertr instance
window.Insertr = {
- // Core functionality
+ // Core functionality (business logic)
core: null,
editor: null,
auth: null,
apiClient: null,
+
+ // UI layer (presentation)
+ controlPanel: null,
// Initialize the library
init(options = {}) {
@@ -23,10 +27,14 @@ window.Insertr = {
// Load CSS first
this.loadStyles(options);
+ // Initialize core business logic modules
this.core = new InsertrCore(options);
this.auth = new InsertrAuth(options);
this.apiClient = new ApiClient(options);
this.editor = new InsertrEditor(this.core, this.auth, this.apiClient, options);
+
+ // Initialize UI layer
+ this.controlPanel = new InsertrControlPanel(this.auth, this.editor, this.apiClient, options);
// Auto-initialize if DOM is ready
if (document.readyState === 'loading') {
@@ -70,12 +78,17 @@ window.Insertr = {
document.head.appendChild(link);
},
- // Start the system - only creates the minimal trigger
+ // Start the system - initializes auth gates and UI
start() {
if (this.auth) {
- this.auth.init(); // Creates footer trigger only
+ this.auth.init(); // Sets up editor gates
}
- // Note: Editor is NOT started here, only when trigger is clicked
+
+ if (this.controlPanel) {
+ this.controlPanel.init(); // Creates unified control panel UI
+ }
+
+ // Note: Editor is NOT started here, only when authentication succeeds
},
// Start the full editor system (called when trigger is activated)
@@ -83,6 +96,11 @@ window.Insertr = {
if (this.editor && !this.editor.isActive) {
this.editor.start();
}
+
+ // Add editing indicators when editor starts
+ if (this.controlPanel) {
+ this.controlPanel.addEditingIndicators();
+ }
},
// Public API methods
diff --git a/lib/src/styles/insertr.css b/lib/src/styles/insertr.css
index 499b116..cc07563 100644
--- a/lib/src/styles/insertr.css
+++ b/lib/src/styles/insertr.css
@@ -85,22 +85,85 @@
}
/* =================================================================
- AUTHENTICATION CONTROLS
+ UNIFIED CONTROL PANEL
================================================================= */
-.insertr-auth-controls {
+.insertr-control-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: var(--insertr-z-overlay);
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: var(--insertr-spacing-sm);
font-family: var(--insertr-font-family);
font-size: var(--insertr-font-size-base);
+ max-width: 280px;
}
-.insertr-auth-btn {
+/* Status Section */
+.insertr-status-section {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: var(--insertr-spacing-xs);
+}
+
+.insertr-status-indicator {
+ background: var(--insertr-bg-primary);
+ color: var(--insertr-text-primary);
+ border: 1px solid var(--insertr-border-color);
+ border-radius: var(--insertr-border-radius);
+ padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--insertr-spacing-xs);
+ font-size: var(--insertr-font-size-sm);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: var(--insertr-transition);
+}
+
+.insertr-status-text {
+ margin: 0;
+ padding: 0;
+ font-weight: 500;
+ color: inherit;
+ white-space: nowrap;
+}
+
+.insertr-status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ transition: var(--insertr-transition);
+}
+
+.insertr-status-visitor {
+ background: var(--insertr-text-muted);
+}
+
+.insertr-status-authenticated {
+ background: var(--insertr-primary);
+}
+
+.insertr-status-editing {
+ background: var(--insertr-success);
+ animation: insertr-pulse 2s infinite;
+}
+
+@keyframes insertr-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+/* Action Buttons Section */
+.insertr-action-section {
+ display: flex;
+ flex-direction: column;
+ gap: var(--insertr-spacing-xs);
+}
+
+.insertr-action-btn {
background: var(--insertr-primary);
color: var(--insertr-text-inverse);
border: none;
@@ -117,21 +180,112 @@
cursor: pointer;
transition: var(--insertr-transition);
line-height: var(--insertr-line-height);
- min-width: 80px;
+ min-width: 120px;
white-space: nowrap;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
-.insertr-auth-btn:hover {
+.insertr-action-btn:hover {
background: var(--insertr-primary-hover);
color: var(--insertr-text-inverse);
text-decoration: none;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
-.insertr-auth-btn:focus {
+.insertr-action-btn:focus {
outline: 2px solid var(--insertr-primary);
outline-offset: 2px;
}
+.insertr-action-btn:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.insertr-action-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* Button Type Variants */
+.insertr-enhance-btn {
+ background: var(--insertr-info);
+}
+
+.insertr-enhance-btn:hover {
+ background: #138496;
+}
+
+.insertr-edit-btn.insertr-edit-active {
+ background: var(--insertr-success);
+}
+
+.insertr-edit-btn.insertr-edit-active:hover {
+ background: #1e7e34;
+}
+
+.insertr-auth-btn.insertr-authenticated {
+ background: var(--insertr-text-secondary);
+}
+
+.insertr-auth-btn.insertr-authenticated:hover {
+ background: var(--insertr-text-primary);
+}
+
+/* =================================================================
+ EDITING INDICATORS
+ Visual feedback for editable content
+ ================================================================= */
+
+.insertr-editing-hover {
+ outline: 2px dashed var(--insertr-primary);
+ outline-offset: 2px;
+ background: rgba(0, 123, 255, 0.05);
+ position: relative;
+ transition: var(--insertr-transition);
+}
+
+.insertr-editing-hover::after {
+ content: '✏️ Click to edit';
+ position: absolute;
+ top: -30px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--insertr-text-primary);
+ color: var(--insertr-text-inverse);
+ padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
+ border-radius: var(--insertr-border-radius);
+ font-size: var(--insertr-font-size-sm);
+ font-family: var(--insertr-font-family);
+ white-space: nowrap;
+ z-index: var(--insertr-z-tooltip);
+ opacity: 0;
+ animation: insertr-tooltip-show 0.2s ease-in-out forwards;
+}
+
+@keyframes insertr-tooltip-show {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(-5px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+/* Hide editing indicators when not in edit mode */
+body:not(.insertr-edit-mode) .insertr-editing-hover {
+ outline: none;
+ background: transparent;
+}
+
+body:not(.insertr-edit-mode) .insertr-editing-hover::after {
+ display: none;
+}
+
/* =================================================================
MODAL OVERLAY & CONTAINER
================================================================= */
@@ -517,9 +671,27 @@
padding: var(--insertr-spacing-sm);
}
- .insertr-auth-controls {
+ .insertr-control-panel {
bottom: 10px;
right: 10px;
+ max-width: 240px;
+ }
+
+ .insertr-action-btn {
+ min-width: 100px;
+ font-size: var(--insertr-font-size-sm);
+ padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
+ }
+
+ .insertr-status-indicator {
+ font-size: 11px;
+ padding: 3px var(--insertr-spacing-xs);
+ }
+
+ .insertr-editing-hover::after {
+ font-size: 11px;
+ padding: 2px var(--insertr-spacing-xs);
+ top: -25px;
}
.insertr-form-actions {
diff --git a/lib/src/ui/control-panel.js b/lib/src/ui/control-panel.js
new file mode 100644
index 0000000..8f7a565
--- /dev/null
+++ b/lib/src/ui/control-panel.js
@@ -0,0 +1,371 @@
+/**
+ * InsertrControlPanel - Unified UI Controller
+ * Handles all presentation layer concerns for the insertr system
+ *
+ * Architecture:
+ * - Pure presentation layer - no business logic
+ * - Event-driven communication with core modules
+ * - Unified control panel design
+ * - Responsive and accessible
+ */
+export class InsertrControlPanel {
+ constructor(auth, editor, apiClient, options = {}) {
+ this.auth = auth;
+ this.editor = editor;
+ this.apiClient = apiClient;
+ this.options = options;
+
+ // UI state
+ this.elements = {};
+ this.isInitialized = false;
+
+ // Bind methods for event listeners
+ this.handleAuthToggle = this.handleAuthToggle.bind(this);
+ this.handleEditToggle = this.handleEditToggle.bind(this);
+ this.handleEnhanceClick = this.handleEnhanceClick.bind(this);
+ }
+
+ /**
+ * Initialize the control panel (called after DOM is ready)
+ */
+ init() {
+ if (this.isInitialized) return;
+
+ console.log('🎨 Initializing Insertr Control Panel');
+ this.createControlPanel();
+ this.setupEventListeners();
+ this.updateVisualState();
+ this.isInitialized = true;
+ }
+
+ /**
+ * Create the unified control panel structure
+ */
+ createControlPanel() {
+ // Check if control panel already exists
+ if (document.getElementById('insertr-control-panel')) {
+ this.cacheElements();
+ return;
+ }
+
+ const controlPanelHtml = `
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Add control panel to page
+ document.body.insertAdjacentHTML('beforeend', controlPanelHtml);
+ this.cacheElements();
+
+ console.log('📱 Control panel created');
+ }
+
+ /**
+ * Cache DOM elements for performance
+ */
+ cacheElements() {
+ this.elements = {
+ controlPanel: document.getElementById('insertr-control-panel'),
+ statusSection: document.getElementById('insertr-status-section'),
+ statusIndicator: document.getElementById('insertr-status-indicator'),
+ statusText: document.querySelector('.insertr-status-text'),
+ statusDot: document.querySelector('.insertr-status-dot'),
+ actionSection: document.getElementById('insertr-action-section'),
+ authToggle: document.getElementById('insertr-auth-toggle'),
+ editToggle: document.getElementById('insertr-edit-toggle'),
+ enhanceBtn: document.getElementById('insertr-enhance-btn')
+ };
+ }
+
+ /**
+ * Setup event listeners for all UI interactions
+ */
+ setupEventListeners() {
+ if (this.elements.authToggle) {
+ this.elements.authToggle.addEventListener('click', this.handleAuthToggle);
+ }
+
+ if (this.elements.editToggle) {
+ this.elements.editToggle.addEventListener('click', this.handleEditToggle);
+ }
+
+ if (this.elements.enhanceBtn) {
+ this.elements.enhanceBtn.addEventListener('click', this.handleEnhanceClick);
+ }
+
+ // Listen for auth state changes
+ if (this.auth && typeof this.auth.on === 'function') {
+ this.auth.on('stateChange', () => this.updateVisualState());
+ }
+
+ // Listen for editor state changes
+ if (this.editor && typeof this.editor.on === 'function') {
+ this.editor.on('modeChange', () => this.updateVisualState());
+ }
+ }
+
+ /**
+ * Handle authentication toggle click
+ */
+ handleAuthToggle() {
+ if (this.auth && typeof this.auth.toggleAuthentication === 'function') {
+ this.auth.toggleAuthentication();
+ this.updateVisualState();
+ }
+ }
+
+ /**
+ * Handle edit mode toggle click
+ */
+ handleEditToggle() {
+ if (this.auth && typeof this.auth.toggleEditMode === 'function') {
+ this.auth.toggleEditMode();
+ this.updateVisualState();
+ }
+ }
+
+ /**
+ * Handle enhance button click
+ */
+ async handleEnhanceClick() {
+ if (!this.elements.enhanceBtn) return;
+
+ const enhanceBtn = this.elements.enhanceBtn;
+ const siteId = window.insertrConfig?.siteId || this.options.siteId || 'demo';
+
+ try {
+ // Show loading state
+ enhanceBtn.textContent = '⏳ Enhancing...';
+ enhanceBtn.disabled = true;
+
+ // Smart server detection for enhance API
+ const isDevelopment = window.location.hostname === 'localhost' ||
+ window.location.hostname === '127.0.0.1';
+ const enhanceUrl = isDevelopment
+ ? `http://localhost:8080/api/enhance?site_id=${siteId}`
+ : `/api/enhance?site_id=${siteId}`;
+
+ // Call enhance API
+ const response = await fetch(enhanceUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${this.auth?.getCurrentUser()?.token || 'mock-token'}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Enhancement failed: ${response.status} ${response.statusText}`);
+ }
+
+ const result = await response.json();
+ console.log('✅ Files enhanced successfully:', result);
+
+ // Show success state briefly
+ enhanceBtn.textContent = '✅ Enhanced!';
+
+ // Reset button after success
+ setTimeout(() => {
+ enhanceBtn.textContent = '🔄 Enhance';
+ enhanceBtn.disabled = false;
+ }, 2000);
+
+ } catch (error) {
+ console.error('❌ Enhancement failed:', error);
+ enhanceBtn.textContent = '❌ Failed';
+
+ // Reset button after error
+ setTimeout(() => {
+ enhanceBtn.textContent = '🔄 Enhance';
+ enhanceBtn.disabled = false;
+ }, 2000);
+ }
+ }
+
+ /**
+ * Update all visual elements based on current state
+ */
+ updateVisualState() {
+ if (!this.isInitialized || !this.elements.statusText) return;
+
+ const isAuthenticated = this.auth ? this.auth.isAuthenticated() : false;
+ const isEditMode = this.auth ? this.auth.isEditMode() : false;
+
+ // Update status indicator
+ this.updateStatusIndicator(isAuthenticated, isEditMode);
+
+ // Update button states
+ this.updateButtonStates(isAuthenticated, isEditMode);
+
+ // Update body classes for global styling
+ this.updateBodyClasses(isAuthenticated, isEditMode);
+ }
+
+ /**
+ * Update status indicator text and visual state
+ */
+ updateStatusIndicator(isAuthenticated, isEditMode) {
+ const { statusText, statusDot } = this.elements;
+
+ if (!statusText || !statusDot) return;
+
+ if (!isAuthenticated) {
+ statusText.textContent = 'Visitor Mode';
+ statusDot.className = 'insertr-status-dot insertr-status-visitor';
+ } else if (isEditMode) {
+ statusText.textContent = 'Editing';
+ statusDot.className = 'insertr-status-dot insertr-status-editing';
+ } else {
+ statusText.textContent = 'Authenticated';
+ statusDot.className = 'insertr-status-dot insertr-status-authenticated';
+ }
+ }
+
+ /**
+ * Update button text, visibility, and states
+ */
+ updateButtonStates(isAuthenticated, isEditMode) {
+ const { authToggle, editToggle, enhanceBtn } = this.elements;
+
+ // Update auth toggle button
+ if (authToggle) {
+ authToggle.textContent = isAuthenticated ? 'Logout' : 'Login as Client';
+ authToggle.className = `insertr-action-btn insertr-auth-btn ${
+ isAuthenticated ? 'insertr-authenticated' : ''
+ }`;
+ }
+
+ // Update edit mode toggle button
+ if (editToggle) {
+ editToggle.style.display = isAuthenticated ? 'inline-block' : 'none';
+ editToggle.textContent = `Edit Mode: ${isEditMode ? 'On' : 'Off'}`;
+ editToggle.className = `insertr-action-btn insertr-edit-btn ${
+ isEditMode ? 'insertr-edit-active' : ''
+ }`;
+ }
+
+ // Update enhance button (dev-mode only)
+ if (enhanceBtn) {
+ enhanceBtn.style.display = isAuthenticated ? 'inline-block' : 'none';
+ }
+ }
+
+ /**
+ * Update body CSS classes for global styling hooks
+ */
+ updateBodyClasses(isAuthenticated, isEditMode) {
+ document.body.classList.toggle('insertr-authenticated', isAuthenticated);
+ document.body.classList.toggle('insertr-edit-mode', isEditMode);
+ }
+
+ /**
+ * Add editing indicators to content elements
+ */
+ addEditingIndicators() {
+ if (!this.editor || !this.editor.core) return;
+
+ const elements = this.editor.core.getAllElements();
+ console.log(`🎨 Adding editing indicators to ${elements.length} elements`);
+
+ elements.forEach(meta => {
+ this.initializeElementVisuals(meta.element);
+ });
+ }
+
+ /**
+ * Initialize visual editing indicators for a single element
+ */
+ initializeElementVisuals(element) {
+ // Add visual indicators
+ element.style.cursor = 'pointer';
+ element.style.position = 'relative';
+
+ // Add hover effects
+ element.addEventListener('mouseenter', () => {
+ if (this.auth && this.auth.isAuthenticated() && this.auth.isEditMode()) {
+ element.classList.add('insertr-editing-hover');
+ }
+ });
+
+ element.addEventListener('mouseleave', () => {
+ element.classList.remove('insertr-editing-hover');
+ });
+ }
+
+ /**
+ * Remove editing indicators (when edit mode is disabled)
+ */
+ removeEditingIndicators() {
+ const elements = document.querySelectorAll('.insertr-editing-hover');
+ elements.forEach(element => {
+ element.classList.remove('insertr-editing-hover');
+ });
+ }
+
+ /**
+ * Show status message to user
+ */
+ showStatusMessage(message, type = 'info', duration = 3000) {
+ // Remove existing status message
+ const existing = document.getElementById('insertr-status-message');
+ if (existing) {
+ existing.remove();
+ }
+
+ const messageHtml = `
+
+ `;
+
+ document.body.insertAdjacentHTML('beforeend', messageHtml);
+
+ // Auto-remove after duration
+ setTimeout(() => {
+ const messageEl = document.getElementById('insertr-status-message');
+ if (messageEl) {
+ messageEl.remove();
+ }
+ }, duration);
+ }
+
+ /**
+ * Destroy the control panel (cleanup)
+ */
+ destroy() {
+ if (this.elements.controlPanel) {
+ this.elements.controlPanel.remove();
+ }
+
+ this.elements = {};
+ this.isInitialized = false;
+
+ // Remove body classes
+ document.body.classList.remove('insertr-authenticated', 'insertr-edit-mode');
+
+ console.log('🗑️ Control panel destroyed');
+ }
+}
\ No newline at end of file
diff --git a/lib/src/ui/Editor.js b/lib/src/ui/editor.js
similarity index 99%
rename from lib/src/ui/Editor.js
rename to lib/src/ui/editor.js
index 153faa4..97a9799 100644
--- a/lib/src/ui/Editor.js
+++ b/lib/src/ui/editor.js
@@ -2,7 +2,7 @@
* Editor - Handles all content types with markdown-first approach
*/
import { markdownConverter } from '../utils/markdown.js';
-import { Previewer } from './Previewer.js';
+import { Previewer } from './previewer.js';
export class Editor {
constructor() {
diff --git a/lib/src/ui/form-renderer.js b/lib/src/ui/form-renderer.js
index 04f8210..d6a06be 100644
--- a/lib/src/ui/form-renderer.js
+++ b/lib/src/ui/form-renderer.js
@@ -2,7 +2,7 @@
* InsertrFormRenderer - Form renderer using markdown-first approach
* Thin wrapper around the Editor system
*/
-import { Editor } from './Editor.js';
+import { Editor } from './editor.js';
export class InsertrFormRenderer {
constructor(apiClient = null) {
diff --git a/lib/src/ui/Previewer.js b/lib/src/ui/previewer.js
similarity index 100%
rename from lib/src/ui/Previewer.js
rename to lib/src/ui/previewer.js