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 = ` -
-
-
- Visitor Mode - -
-
- -
- `; - - 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 = ` +
+ +
+
+ Visitor Mode + +
+
+ + +
+ + + +
+
+ `; + + // 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 = ` +
+

${message}

+
+ `; + + 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