@quartal/bridge-client
Version: 
Universal client library for embedding applications with URL-configurable transport support (iframe, postMessage) and framework adapters for Angular and Vue
1,323 lines (1,083 loc) • 38.2 kB
Markdown
# Quartal Bridge Client
Universal client library for embedding applications with URL-configurable transport support and framework adapters for Angular and Vue.
## Features
- 🚀 **Framework-agnostic** - Works without Angular/Vue dependencies
- 🔧 **Framework Adapters** - Easy integration for Angular and Vue
- 📦 **Treeshakable** - Only used parts included in bundle
- 🔄 **Multiple Transports** - Iframe and PostMessage communication
- 🌐 **URL Configuration** - Configure transport via URL parameters
- 🎯 **Singleton pattern** - One instance per application
- 🐛 **Debug logging** - Comprehensive logging and error handling
- 📱 **TypeScript** - Full TypeScript support
- 🎨 **Named instances** - Multiple isolated instances for complex applications
## Installation
```bash
npm install @quartal/bridge-client
# or
yarn add @quartal/bridge-client
# or
pnpm add @quartal/bridge-client
```
## Quick Start
### URL-Based Transport Configuration (Recommended)
Child applications automatically detect transport configuration from URL parameters:
```html
<!-- Iframe transport (default) -->
<iframe src="https://your-app.com/quartal-accounting-embed"></iframe>
<!-- PostMessage transport for popups -->
<iframe src="https://your-app.com/quartal-accounting-embed?transport=postmessage&debug=true"></iframe>
```
### Vue.js Child Application
```typescript
import { QuartalVueAdapter } from '@quartal/bridge-client';
// Automatically uses URL parameters for transport configuration
const adapter = new QuartalVueAdapter(router);
const childClient = adapter.createChildClient({
  appPrefix: 'VUE-APP',
  autoConnect: true
}, {
  onInited: (data) => console.log('Connected to parent:', data),
  onOpenUrl: (url) => router.push(url)
});
```
### Angular Child Application
```typescript
import { QuartalAngularAdapter } from '@quartal/bridge-client';
// Automatically uses URL parameters for transport configuration  
```typescript
const adapter = new QuartalAngularAdapter(router);
```
const childClient = adapter.createChildClient({
  appPrefix: 'NG-APP',
  autoConnect: true
}, {
  onInited: (data) => console.log('Connected to parent:', data),
  onRedirect: (url) => this.router.navigate(url)
});
```
## URL Transport Configuration
Configure transport behavior via URL parameters without code changes:
### Supported Parameters
| Parameter | Values | Default | Description |
|-----------|--------|---------|-------------|
| `transport` | `iframe`, `postmessage` | `iframe` | Transport type |
| `debug` | `true`, `false` | `false` | Enable debug logging |
| `trace` | `true`, `false` | `false` | Enable trace logging |
| `targetOrigin` | URL or `*` | `*` | PostMessage target origin |
| `channel` | string | `quartal-bridge` | PostMessage channel |
| `timeout` | number | `10000` | Connection timeout (ms) |
### Examples
```html
<!-- Standard iframe embedding -->
<iframe src="https://app.com/embed"></iframe>
<!-- PostMessage for popups with debug -->
<iframe src="https://app.com/embed?transport=postmessage&debug=true"></iframe>
<!-- Secure PostMessage with specific origin -->
<iframe src="https://app.com/embed?transport=postmessage&targetOrigin=https://parent.com"></iframe>
```
### JavaScript Popup
```javascript
const popup = window.open(
  'https://app.com/embed?transport=postmessage&targetOrigin=' + window.location.origin,
  'quartal-app',
  'width=1200,height=800'
);
```
For detailed configuration options, see [URL Transport Configuration](./docs/URL_TRANSPORT_CONFIG.md).
## Architecture Patterns
### Multi-level iframe Communication
The Quartal Client supports complex iframe chains with different integration patterns:
```
partner-ui (Framework agnostic parent)
  ↓ parentClientManager.initializeParentClient()
  ↓
quartal-invoicing-ui (Angular Hybrid: child + parent)
  ↓ QuartalAngularAdapter.createChildClient() → AppService → QuartalEventBridge → IFrameComponent → parentClientManager.initializeParentClient()
  ↓
quartal-accounting-embed (Vue child)
  ↓ useAppService() composable with ChildClient
```
### Integration Patterns
#### 1. **Parent Applications** (Framework-agnostic)
- **Pattern**: Use `parentClientManager` directly
- **Benefits**: Framework-agnostic, singleton lifecycle management
- **Used by**: Partners' own UI and quartal-invoicing-ui's iframe components
#### 2. **Angular Child Applications** 
- **Pattern**: Use `QuartalAngularAdapter`
- **Benefits**: Angular router/service integration, dependency injection
- **Used by**: quartal-invoicing-ui (later partners' own UI)
#### 3. **Vue Child Applications**
- **Pattern**: Direct composable usage or `QuartalVueAdapter`
- **Benefits**: Vue composable pattern, reactive integration
- **Used by**: quartal-accounting-embed (later partners' own UI)
### Event Flow Examples
All events follow the same technical path: **Child Client → QuartalEventBridge → Parent Client**
1. **Height Changes**: 
   - quartal-accounting-embed (DOM observer) → `childClient.sendHeightChange()` 
   - → quartal-invoicing-ui (`QuartalEventBridge` forwarding) 
   - → partner-ui (`onHeightChange` callback)
2. **Dialog States**: 
   - quartal-accounting-embed (QDialog component) → `childClient.sendDialogChanged()` 
   - → quartal-invoicing-ui (`QuartalEventBridge` forwarding) 
   - → partner-ui (`onDialogChanged` callback)
3. **HTTP Status**: 
   - quartal-accounting-embed (HTTP interceptor) → `childClient.sendPendingRequest()` 
   - → quartal-invoicing-ui (`QuartalEventBridge` forwarding) 
   - → partner-ui (`onPendingRequest` callback)
### Hybrid Application Pattern
For applications that act as both parent and child (like quartal-invoicing-ui):
```typescript
import { QuartalEventBridge, INTERNAL_EVENTS } from '@quartal/client';
// quartal-invoicing-ui: Acts as child to partner applications AND parent to child applications
export class AppService {
  private quartalClient: ChildClient; // Communicates UP to partner-ui
  private eventBridge: QuartalEventBridge; // Internal forwarding
  initializeClient() {
    // Use Angular adapter for child functionality
    const quartalAdapter = new QuartalAngularAdapter(
      this.router
    );
    this.quartalClient = quartalAdapter.createChildClient({
      debug: false,
      appPrefix: 'MyApp'
    }, {
      onInited: (data) => this.handleInitialization(data),
      onLogout: () => this.handleLogout()
    });
    // Set up internal event forwarding
    this.setupEventForwarding();
  }
  // Forward events from internal components to parent (partner-ui)
  private setupEventForwarding() {
    this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
      if (this.quartalClient?.isReady()) {
        this.quartalClient.sendDialogChanged(message.data.isOpen);
      }
    });
    
    this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_HEIGHT_CHANGE, (message) => {
      if (this.quartalClient?.isReady()) {
        this.quartalClient.sendHeightChange(message.data.height);
      }
    });
  }
}
export class IFrameComponent {
  private parentClient: ParentClient; // Communicates DOWN to child applications
  private eventBridge: QuartalEventBridge; // Internal forwarding
  // Initialize with parentClientManager for parent functionality
  private initializeClient() {
    this.parentClient = parentClientManager.initializeParentClient(
      {
        iframeElement: this.iframe.nativeElement,
        appPrefix: 'MyApp',
        debug: true,
        user: this.getCurrentUser()
      },
      {
        onHeightChange: (height) => {
          // Forward to internal event bridge → AppService → partner-ui
          this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_HEIGHT_CHANGE, { height }, 'iframe-component');
        },
        onDialogChanged: (dialogState) => {
          this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, { isOpen: dialogState.opened }, 'iframe-component');
        },
        onPendingRequest: (pending) => {
          this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_PENDING_REQUEST, { pending }, 'iframe-component');
        }
      }
    );
  }
}
```
## Quick Start
### For Parent Applications
```typescript
import { parentClientManager } from '@quartal/client';
const parentClient = parentClientManager.initializeParentClient(
  {
    iframeElement: document.getElementById('quartal-iframe') as HTMLIFrameElement,
    appPrefix: 'MyApp',
    user: {
      id: 'user123',
      email: 'user@example.com', 
      name: 'John Doe',
      phone: '+358401234567'
    }
  },
  {
    onInited: (data) => console.log('Child initialized:', data),
    onHeightChange: (height) => console.log('Height changed:', height)
  }
);
```
### For Angular Child Applications
```typescript
import { QuartalAngularAdapter } from '@quartal/client';
// In your service
const adapter = new QuartalAngularAdapter(router);
const childClient = adapter.createChildClient({
  appPrefix: 'MyApp'
}, {
  onInited: (data) => console.log('Ready:', data)
});
```
### For Vue Child Applications
```typescript
import { QuartalVueAdapter } from '@quartal/client';
// In your composable
const adapter = new QuartalVueAdapter(router);
const childClient = adapter.createChildClient({
  appPrefix: 'MyApp'
}, {
  onInited: () => console.log('Vue child ready')
});
```
## Detailed Usage Examples
> 📁 **Complete examples**: For production-ready, copy-paste examples see the [`examples/`](./examples/) directory.
### Basic Usage (without framework adapters)
```typescript
import { parentClientManager, ChildClient } from '@quartal/client';
// Parent client using manager
const parentClient = parentClientManager.initializeParentClient(
  {
    iframeElement: document.getElementById('quartal-iframe') as HTMLIFrameElement,
    appPrefix: 'MyApp',
    debug: true,
    trace: false,
    user: {
      id: 'user123',
      email: 'user@example.com', 
      name: 'John Doe',
      phone: '+358401234567'
    }
  },
  {
    onInited: (data) => console.log('Child initialized:', data),
    onHeightChange: (height) => console.log('Height changed:', height),
    onPendingRequest: (pending) => console.log('Pending request:', pending),
    onDialogChanged: (isOpen) => console.log('Dialog state:', isOpen),
    onAlert: (alert) => console.log('Alert:', alert),
    onUrlChange: (url) => console.log('URL changed:', url)
  }
);
// Child client
const childClient = new ChildClient({
  debug: true,
  callbacks: {
    onLogout: () => console.log('Logout requested'),
    onReload: () => window.location.reload()
  }
});
```
### Angular Integration
```typescript
// quartal-iframe.component.ts (Angular Parent using parentClientManager)
import { Component, ElementRef, ViewChild, OnDestroy } from '@angular/core';
import { parentClientManager, ParentClient } from '@quartal/client';
@Component({
  selector: 'app-quartal-iframe',
  template: '<iframe #quartalIframe id="quartalIframe" [src]="iframeUrl"></iframe>'
})
export class QuartalIframeComponent implements OnDestroy {
  @ViewChild('quartalIframe') iframe!: ElementRef<HTMLIFrameElement>;
  
  private parentClient?: ParentClient;
  ngAfterViewInit() {
    this.initializeClient();
  }
  private async initializeClient() {
    const iframeElement = this.iframe.nativeElement;
    
    try {
      this.parentClient = parentClientManager.initializeParentClient(
        {
          iframeElement,
          appPrefix: 'MyApp',
          debug: false,
          trace: false,
          showNavigation: false,
          showTopNavbar: false,
          showFooter: false,
          showLoading: false,
          showMessages: false,
          customActions: this.getCustomActions(),
          customEntities: this.getCustomEntities(),
          customTabs: this.getCustomTabs(),
          customTables: this.getCustomTables(),
          user: this.user ? {
            id: String(this.user.id ?? this.user.username),
            email: this.user.email ?? this.user.username,
            name: this.user.name,
            phone: this.user.phone || 
          } : undefined
        },
        {
          onInited: (data) => {
            console.log('(MyApp) Child initialized:', data);
            this.openUrl(this.router.routerState.snapshot.url);
          },
          onHeightChange: (height: number) => {
            this.handleHeightChange(height);
          },
          onPendingRequest: (pending: boolean) => {
            this.httpStatus.setPendingRequests(pending);
          },
          onDialogChanged: (dialogState: any) => {
            this.handleDialogChanged(dialogState);
          },
          onAlert: (alert: any) => {
            this.handleAlert(alert);
          },
          onUrlChange: (url: string) => {
            this.handleUrlChange(url);
          },
          onMouseClick: () => {
            this.iframeService.setClick();
          },
          onCustomAction: (action: string, data: any) => {
            this.openParentAction(action, data);
          },
          onError: (error: Error) => {
            console.error('(MyApp) Parent client error:', error);
          }
        }
      );
      // Store globally for service access
      this.iframeService.setEmbedContainer(this.parentClient);
      
    } catch (error) {
      console.error('(MyApp) Failed to initialize parent client:', error);
    }
  }
  private handleHeightChange(height: number) {
    if (!this.dialogOpened) {
      setTimeout(() => {
        this.height = height + 'px';
      }, 0);
    }
  }
  private handleDialogChanged(dialogState: any) {
    if (dialogState.opened) {
      this.renderer.addClass(document.body, 'dialog-opened');
      this.originalHeight = this.height && this.height != '0px' ? this.height : '100vh';
      this.height = '100vh';
      this.dialogOpened = true;
    } else {
      this.renderer.removeClass(document.body, 'dialog-opened');
      this.height = this.originalHeight;
      this.dialogOpened = false;
    }
  }
  ngOnDestroy() {
    // Unregister from parent client manager
    parentClientManager.unregisterIframeComponent();
    this.iframeService.setEmbedContainer(null);
  }
}
// app.service.ts (Angular Child using Adapter and EventBridge)
import { Injectable } from '@angular/core';
import { 
  QuartalAngularAdapter, 
  ChildClient, 
  QuartalEventBridge,
  INTERNAL_EVENTS 
} from '@quartal/client';
@Injectable({ providedIn: 'root' })
export class AppService {
  private quartalClient?: ChildClient;
  private eventBridge = QuartalEventBridge.getInstance('quartal-invoicing-ui');
  constructor(
    private router: Router
  ) {}
  initializeClient() {
    const quartalAdapter = new QuartalAngularAdapter(
      this.router
    );
    this.quartalClient = quartalAdapter.createChildClient({
      debug: false,
      trace: false,
      appPrefix: 'MyApp',
      autoConnect: true
    }, {
      onInited: (data) => {
        console.log('(MyApp) Child client initialized:', data);
        this.setAppSettings(data);
        this.initializeEventListeners();
      },
      onOpenUrl: (url: string) => {
        console.log('(MyApp) Open URL in parent:', url);
        this.openUrl(url);
      },
      onRedirect: (url: string[]) => {
        console.log('(MyApp) Redirect URL in parent:', url);
        this.redirectTo(url);
      },
      onLogout: () => {
        console.log('(MyApp) Logout from parent');
        localStorage.removeItem('idToken');
      }
    });
    // Set up event forwarding for hybrid apps
    this.setupEventForwarding();
  }
  private setupEventForwarding() {
    // Forward internal events to parent using imported INTERNAL_EVENTS
    this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
      if (this.quartalClient?.isReady()) {
        this.quartalClient.sendDialogChanged(message.data.isOpen);
      }
    });
    this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_PENDING_REQUEST, (message) => {
      if (this.quartalClient?.isReady()) {
        this.quartalClient.sendPendingRequest(message.data.pending);
      }
    });
    this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_ALERT, (message) => {
      if (this.quartalClient?.isReady()) {
        this.quartalClient.sendAlert(message.data);
      }
    });
    this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_TITLE_CHANGE, (message) => {
      if (this.quartalClient?.isReady()) {
        this.quartalClient.sendTitleChange(message.data);
      }
    });
    this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_MOUSE_CLICK, (message) => {
      if (this.quartalClient?.isReady()) {
        this.quartalClient.sendMouseClick();
      }
    });
  }
}
```
### Vue Integration
```typescript
// Vue Child using QuartalVueAdapter (recommended pattern)
// useAppService.ts
import { getCurrentInstance } from 'vue';
import { useRouter } from 'vue-router';
import type { Router } from 'vue-router';
import { QuartalVueAdapter } from '@quartal/client';
// Global singleton state
let globalVueAdapter: QuartalVueAdapter | null = null;
let routerInstance: Router | null = null;
function initializeVueAdapter(onReady?: () => void): QuartalVueAdapter {
  if (globalVueAdapter) {
    console.debug('(MyApp) Vue adapter already initialized');
    if (onReady) onReady();
    return globalVueAdapter;
  }
  console.debug('(MyApp) Creating new QuartalVueAdapter...');
  
  try {
    // QuartalVueAdapter constructor takes (router)
    globalVueAdapter = new QuartalVueAdapter(routerInstance);
    // Create child client with callbacks
    const childClient = globalVueAdapter.createChildClient({
      debug: false,
      trace: false,
      appPrefix: 'QA',
      autoConnect: true
    }, {
      onInited: (data) => {
        console.debug('(QA) Child client initialized:', data);
        // Handle app settings from parent
        if (onReady) onReady();
      },
      onOpenUrl: (url: string) => {
        console.log('(QA) Open URL in parent:', url);
        if (routerInstance) {
          routerInstance.push(url);
        }
      },
      onRedirect: (url: string[]) => {
        console.log('(QA) Redirect URL in parent:', url);
        if (routerInstance) {
          routerInstance.push(url.join('/'));
        }
      },
      onLogout: () => {
        console.log('(QA) Logout from parent');
        localStorage.removeItem('idToken');
      }
    });
    console.debug('(MyApp) QuartalVueAdapter initialized successfully');
    return globalVueAdapter;
  } catch (error) {
    console.error('(MyApp) Error initializing QuartalVueAdapter:', error);
    throw error;
  }
}
// Main composable function
export function useAppService(onReady?: () => void) {
  // Get router instance if we're in a Vue component context
  const currentInstance = getCurrentInstance();
  if (currentInstance && !routerInstance) {
    try {
      routerInstance = useRouter();
    } catch (error) {
      console.warn('(MyApp) Could not get router instance:', error);
    }
  }
  // Initialize or update the adapter
  const adapter = initializeVueAdapter(onReady);
  return {
    adapter,
    openUrl: (url: string) => {
      const client = adapter?.getChildClient();
      if (client) {
        try {
          client.navigate(url);
        } catch (error) {
          console.warn('(MyApp) Navigation failed:', error);
          // Fallback: send as fast action to parent
          client.sendOpenFastAction({ link: url });
        }
      } else {
        console.warn('(MyApp) No adapter available for openUrl');
      }
    },
    notifyRouteChange: (url: string) => {
      const client = adapter?.getChildClient();
      if (client && typeof client.sendUrlChange === 'function') {
        client.sendUrlChange(url);
      }
    },
    setMouseClicked: () => {
      const client = adapter?.getChildClient();
      if (client && typeof client.sendMouseClick === 'function') {
        client.sendMouseClick();
      }
    },
    destroy: () => {
      if (globalVueAdapter) {
        globalVueAdapter.destroy();
        globalVueAdapter = null;
      }
    }
  };
}
// Usage in Vue Component
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { useSession } from '@quartal/ui-accounting';
import { useAppService } from './services/useAppService';
const { user } = useSession();
// Initialize app service with Vue adapter
let appService = useAppService(() => {
  console.log('(MyApp) Vue child client ready');
  
  // Expose the client globally after it's ready
  if (appService.client) {
    (window as any).quartalClient = appService.client;
    (window as any).childClient = appService.client;
  }
});
</script>
```
## QuartalEventBridge - For Hybrid Applications
Hybrid applications (that act as both parent and child) can use `QuartalEventBridge` for internal event forwarding.
### Use Cases
- **quartal-invoicing-ui**: iframe.component (parent) → app.service (child) → partner application
- **Future hybrid applications**
### Basic Example
```typescript
import { QuartalEventBridge, INTERNAL_EVENTS } from '@quartal/client';
// Create or get instance for application
const eventBridge = QuartalEventBridge.getInstance('partner-app');
// Parent component sends events
export class IFrameComponent {
  private eventBridge = QuartalEventBridge.getInstance('partner-app');
  private handleAlert(alert: any) {
    // Local handling first
    this.showLocalAlert(alert);
    
    // Forward to child component internally
    this.eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_ALERT, alert, 'iframe-component');
  }
}
// Child component listens and forwards upward
export class AppService {
  private eventBridge = QuartalEventBridge.getInstance('partner-app');
  private unsubscribers: (() => void)[] = [];
  constructor() {
    this.setupEventForwarding();
  }
  private setupEventForwarding() {
    // Listen to internal events and forward to parent
    this.unsubscribers.push(
      this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_ALERT, (message) => {
        if (this.quartalClient?.isReady()) {
          this.quartalClient.sendAlert(message.data);
        }
      })
    );
    this.unsubscribers.push(
      this.eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
        if (this.quartalClient?.isReady()) {
          this.quartalClient.sendDialogChanged(message.data.isOpen);
        }
      })
    );
  }
  destroy() {
    // Cleanup listeners
    this.unsubscribers.forEach(unsub => unsub());
  }
}
```
### Named Instances
```typescript
// Different instances for different applications
const partnerAppBridge = QuartalEventBridge.getInstance('partner-app');
const invoicingBridge = QuartalEventBridge.getInstance('quartal-invoicing-ui');
const embedBridge = QuartalEventBridge.getInstance('quartal-accounting-embed');
// List active instances
console.log(QuartalEventBridge.getActiveInstances()); 
// ['quartal-invoicing-ui', 'quartal-accounting-embed']
// Destroy specific instance
QuartalEventBridge.destroyInstance('quartal-accounting-embed');
```
### Event Types
```typescript
// Supported event types - INTERNAL_EVENTS values or custom strings
type EventType = keyof typeof INTERNAL_EVENTS | string;
// Sending events
eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, { isOpen: true }, 'iframe-component');
eventBridge.sendEvent(INTERNAL_EVENTS.PARENT_CHILD_PENDING_REQUEST, { pending: false }, 'http-interceptor');
eventBridge.sendEvent('custom', { repositoryId: '2025' }, 'period-selector');
// Listening to events  
eventBridge.on(INTERNAL_EVENTS.PARENT_CHILD_DIALOG_CHANGED, (message) => {
  console.log('Dialog event:', message.data, 'from:', message.source);
});
eventBridge.on('custom', (message) => {
  if (message.data.repositoryId) {
    console.log('Repository changed to:', message.data.repositoryId);
  }
});
```
## Height Detection and Reporting
Automatic height detection for smooth iframe resizing:
```typescript
// In child application
function setupHeightDetection(client: ChildClient) {
  let lastHeight = 0;
  let debounceTimer: any = null;
  const detectHeight = () => {
    const currentHeight = document.body.scrollHeight;
    
    // Only update if height changed significantly (5px threshold)
    if (Math.abs(currentHeight - lastHeight) > 5) {
      clearTimeout(debounceTimer);
      
      // Immediate update for significant increases (new content)
      if (currentHeight > lastHeight + 50) {
        lastHeight = currentHeight;
        client.sendHeightChange(currentHeight);
        return;
      }
      // Debounced update for smaller changes
      debounceTimer = setTimeout(() => {
        lastHeight = currentHeight;
        client.sendHeightChange(currentHeight);
      }, 800);
    }
  };
  // Monitor DOM changes
  const observer = new MutationObserver(detectHeight);
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['style', 'class']
  });
  window.addEventListener('resize', detectHeight);
  
  // Initial detection
  detectHeight();
}
```
## HTTP Status Tracking
Track pending requests and notify parent:
```typescript
// HTTP status composable (Vue)
export function useHttpStatus() {
  const pendingRequests = ref(new Set<string>());
  const pendingRequest = computed(() => pendingRequests.value.size > 0);
  function addRequest(id: string) {
    pendingRequests.value.add(id);
  }
  function removeRequest(id: string) {
    pendingRequests.value.delete(id);
  }
  return {
    pendingRequest: readonly(pendingRequest),
    addRequest,
    removeRequest
  };
}
// HTTP interceptor setup
export function setupHttpInterceptor() {
  const { addRequest, removeRequest } = useHttpStatus();
  
  const originalFetch = window.fetch;
  window.fetch = async (...args) => {
    const requestId = `fetch_${Date.now()}_${Math.random()}`;
    
    addRequest(requestId);
    
    try {
      const response = await originalFetch(...args);
      return response;
    } finally {
      removeRequest(requestId);
    }
  };
}
```
## Events
### Parent → Child (Most Common)
- `CHILD_PARENT_READY` - Parent is ready
- `CHILD_PARENT_LOGOUT` - Logout request  
- `CHILD_PARENT_RELOAD` - Reload request
- `CHILD_PARENT_OPEN_URL` - Navigate to URL
- `CHILD_PARENT_REDIRECT` - Redirect to URL
- `CHILD_PARENT_FAST_ACTIONS` - Fast actions available
### Child → Parent (Most Common)
- `PARENT_CHILD_READY` - Child is ready
- `PARENT_CHILD_INIT` - Child initialized
- `PARENT_CHILD_URL_CHANGE` - URL changed
- `PARENT_CHILD_HEIGHT_CHANGE` - Height changed
- `PARENT_CHILD_PENDING_REQUEST` - HTTP request status
- `PARENT_CHILD_DIALOG_CHANGED` - Dialog state changed
- `PARENT_CHILD_MOUSE_CLICK` - Mouse activity
- `PARENT_CHILD_ALERT` - Alert message
- `PARENT_CHILD_ERROR` - Error occurred
> **Note**: This list shows the most commonly used events. For a complete list of all available events, see the `QUARTAL_EVENTS` constant in the TypeScript definitions.
## Debug Logging
```typescript
// Enable debug logging with debug: true
const client = new ParentClient({
  debug: true,
  trace: true, // More detailed logging
  instanceName: 'MyApp' // App prefix for logging
});
// Logs show: (MyApp) [inst_1] [DEBUG] Message
```
## Named Instances
```typescript
// One instance per instanceName
const client1 = new ParentClient({ instanceName: 'App1', ... });
const client2 = new ParentClient({ instanceName: 'App1', ... }); // Returns same instance
const client3 = new ParentClient({ instanceName: 'App2', ... }); // New instance
```
## Development
```bash
# Install dependencies
pnpm install
# Build TypeScript
pnpm build
# Test
pnpm test
# Lint
pnpm lint
```
## Publishing
### Full Deployment (Recommended for Monorepo)
For complete deployment to both git subtree and npm registry (works only in monorepo environment):
```bash
# Deploy to both repositories (recommended workflow in monorepo)
./scripts/deploy-bridge-client.sh [patch|minor|major]
# Preview deployment without making changes  
./scripts/deploy-bridge-client.sh patch --dry-run
# Selective deployment
./scripts/deploy-bridge-client.sh patch --git-only    # Git subtree only (monorepo only)
./scripts/deploy-bridge-client.sh patch --npm-only    # npm registry only
```
### Standalone Repository Publishing
When bridge-client is cloned as a standalone repository from GitHub:
```bash
# Only npm publishing is supported in standalone mode
./scripts/deploy-bridge-client.sh patch --npm-only
# Git operations are automatically disabled in standalone mode
# The script will detect this and only perform npm publishing
```
### Package.json Scripts
```bash
# Version-specific deployment (works differently based on environment)
pnpm run deploy:patch    # Deploy patch version (git+npm in monorepo, npm-only in standalone)
pnpm run deploy:minor    # Deploy minor version (git+npm in monorepo, npm-only in standalone)
pnpm run deploy:major    # Deploy major version (git+npm in monorepo, npm-only in standalone)
# Selective deployment
pnpm run deploy:git      # Deploy only to git subtree (monorepo only - will fail in standalone)
pnpm run deploy:npm      # Deploy only to npm registry (works in both environments)
```
## Best Practices
### 1. Choose the Right Pattern
**Parent Applications:**
- ✅ Use `parentClientManager.initializeParentClient()`
- ✅ Framework-agnostic singleton management
- ✅ Handles iframe lifecycle automatically
**Angular Child Applications:**
- ✅ Use `QuartalAngularAdapter.createChildClient()`
- ✅ Automatic router and service integration
- ✅ Dependency injection support
**Vue Child Applications:**
- ✅ Use `QuartalVueAdapter.createChildClient()`
- ✅ Automatic router and service integration
- ✅ Vue composable pattern, reactive integration
- ✅ Reactive integration with Vue ecosystem
### 2. Instance Naming
- Use descriptive app prefixes: `'MyApp'` (partner's application), `'Q'` (quartal invoicing), `'QA'` (quartal accounting)
- Consistent naming across all clients in the same application
### 3. Error Handling
```typescript
const client = new ChildClient({
  callbacks: {
    onInited: (config) => {
      try {
        // Your initialization logic
      } catch (error) {
        client.sendError(error);
      }
    }
  }
});
// Or with Angular adapter
const adapter = new QuartalAngularAdapter(router);
const client = adapter.createChildClient(config, {
  onInited: (data) => {
    try {
      this.handleInitialization(data);
    } catch (error) {
      console.error('Initialization failed:', error);
    }
  }
});
```
### 4. Cleanup
```typescript
// Angular parent cleanup
ngOnDestroy() {
  // Unregister from parent client manager
  parentClientManager.unregisterIframeComponent();
  
  // Clear service references
  this.iframeService.setEmbedContainer(null);
  
  // Cleanup event bridges
  this.eventBridge?.destroy();
}
// Angular child cleanup
ngOnDestroy() {
  // Adapter handles cleanup automatically
  this.quartalAdapter?.destroy();
}
// Vue child cleanup  
onBeforeUnmount(() => {
  childClient?.destroy();
  // or if using adapter
  adapter?.destroy();
});
```
### 5. Height Detection
- Use debouncing for performance
- Set meaningful thresholds (5px minimum change)
- Monitor DOM mutations for dynamic content
### 6. HTTP Status Tracking
- Track all async operations that affect UI state
- Use unique request IDs
- Clean up completed requests immediately
### 7. Publishing and Versioning
- **Always test locally** before publishing
- **Follow semantic versioning** for releases
- **Update documentation** when adding new features  
- **Check consuming projects** after publishing new versions
- Use `--dry-run` flag to preview what would be published
## Troubleshooting
### Common Issues
1. **Events not received**
   - Check if client is ready: `client.isReady()`
   - Verify instance names match
   - Enable debug logging
2. **Height not updating**
   - Ensure MutationObserver is set up
   - Check CSS transitions don't interfere
   - Verify threshold values
3. **Multiple instances**
   - Use named instances for isolation
   - Clean up properly on component destruction
### Debug Tips
```typescript
// Enable detailed logging
const client = parentClientManager.initializeParentClient({
  iframeElement: document.getElementById('quartal-iframe'),
  appPrefix: 'MyApp',
  debug: true,
  trace: true
}, {
  onError: (error) => console.error('(MyApp) Parent client error:', error)
});
// Check client state
console.log('Client state:', client.getState());
console.log('Is ready:', client.isReady());
console.log('Is connected:', client.isConnected());
// Monitor events
QuartalEventBridge.getInstance('your-app').on('*', (message) => {
  console.log('Event:', message.type, message.data, 'from:', message.source);
});
```
## API Reference
> 💡 **Production Examples**: For complete, copy-paste ready implementations, see [`examples/`](./examples/) directory with Angular and Vue examples.
### ParentClientManager
```typescript
import { parentClientManager } from '@quartal/client';
// Initialize parent client with manager
const parentClient = parentClientManager.initializeParentClient(
  config: ParentClientConfig,
  callbacks: ParentClientCallbacks
): ParentClient;
interface ParentClientConfig {
  iframeElement: HTMLIFrameElement;
  appPrefix?: string; // Application prefix for logging (e.g., 'MyApp', 'QA')
  debug?: boolean;
  trace?: boolean;
  showNavigation?: boolean;
  showTopNavbar?: boolean;
  showFooter?: boolean;
  showLoading?: boolean;
  showMessages?: boolean;
  customActions?: CustomActions;
  customEntities?: CustomEntity[];
  customTabs?: CustomTabs;
  customTables?: CustomTables;
  user?: {
    id: string;
    email: string;
    name: string;
  };
}
interface ParentClientCallbacks {
  onInited?: (data: any) => void;
  onFastActions?: (actions: any[]) => void;
  onFastActionCallback?: (callback: any) => void;
  onUrlChange?: (url: string) => void;
  onHeightChange?: (height: number) => void;
  onAlert?: (alert: any) => void;
  onTitleChange?: (title: string) => void;
  onMouseClick?: () => void;
  onFetchData?: (request: any) => void;
  onError?: (error: Error) => void;
  onPendingRequest?: (pending: boolean) => void;
  onDialogChanged?: (dialogState: any) => void;
  onDataChanged?: (data: any) => void;
  onUserNotifications?: (notifications: any[]) => void;
  onMakePayment?: (payment: any) => void;
  onCustomAction?: (action: string, data: any) => void;
}
// Manager methods
parentClientManager.unregisterIframeComponent(): void; // Cleanup on component destroy
```
### ParentClient (Direct Usage)
```typescript
interface ParentClientConfig {
  iframeElement: HTMLIFrameElement;
  debug?: boolean;
  trace?: boolean;
  instanceName?: string; // For named instances in hybrid apps
  callbacks?: {
    onReady?: () => void;
    onHeightChange?: (height: number) => void;
    onPendingRequest?: (pending: boolean) => void;
    onDialogChanged?: (isOpen: boolean) => void;
    onMouseClick?: () => void;
    onUrlChange?: (url: string) => void;
    onAlert?: (alert: any) => void;
    onError?: (error: Error) => void;
  };
}
class ParentClient {
  constructor(config: ParentClientConfig);
  
  // Core methods
  isReady(): boolean;
  isConnected(): boolean;
  destroy(): void;
  
  // Adapter management
  setAdapters(adapters: Record<string, any>): void;
  
  // Communication methods
  sendToChild(message: any): void;
  
  // State management
  getState(): ParentClientState;
}
```
### ChildClient
```typescript
interface ChildClientConfig {
  debug?: boolean;
  trace?: boolean;
  instanceName?: string; // For named instances in hybrid apps
  callbacks?: {
    onInited?: (config: any) => void;
    onLogout?: () => void;
    onReload?: () => void;
    onNavigate?: (url: string) => void;
  };
}
class ChildClient {
  constructor(config: ChildClientConfig);
  
  // Core methods
  isReady(): boolean;
  isConnected(): boolean;
  destroy(): void;
  
  // Adapter management
  setAdapters(adapters: Record<string, any>): void;
  
  // Communication methods
  sendInited(data: any): void;
  sendHeightChange(height: number): void;
  sendPendingRequest(pending: boolean): void;
  sendDialogChanged(isOpen: boolean): void;
  sendMouseClick(): void;
  sendUrlChange(url: string): void;
  sendAlert(alert: any): void;
  sendMakePayment(payment: any): void;
  sendError(error: Error): void;
  navigate(url: string): void;
  
  // State management
  getState(): ChildClientState;
}
```
### Adapters
```typescript
// Angular Adapter (Used in child applications)
import { QuartalAngularAdapter } from '@quartal/client';
const adapter = new QuartalAngularAdapter(router);
// Create child client with framework integration
const childClient = adapter.createChildClient({
  debug: false,
  trace: false,
  appPrefix: 'Q'
}, {
  onInited: (data) => console.log('(MyApp) Child initialized:', data),
  onLogout: () => this.authService.logout()
});
// Vue Router Adapter (Used directly or via QuartalVueAdapter)
class VueRouterAdapter {
  constructor(router: Router);
  onUrlChange(callback: (url: string) => void): () => void;
}
// Vue Adapter (Optional pattern)
import { QuartalVueAdapter } from '@quartal/client';
const adapter = new QuartalVueAdapter(router);
const childClient = adapter.createChildClient(config, callbacks);
// Angular Router Adapter (Used internally by QuartalAngularAdapter)
class AngularRouterAdapter {
  constructor(router: Router);
  onUrlChange(callback: (url: string) => void): Subscription;
}
```
## License
MIT
## Contributing
We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for development guidelines.
1. Fork the repository
2. Create a feature branch
3. Make your changes and add tests
4. Submit a pull request
### For Maintainers
Internal deployment notes can be found in package.json scripts.
## Changelog
### v1.0.0
- 🎉 Initial release
- ✨ Basic parent-child communication
- ✨ Angular and Vue adapters
- ✨ QuartalEventBridge for hybrid applications
- ✨ Named instances support
- ✨ Automatic height detection and reporting
- ✨ HTTP status tracking
- 🐛 Debug logging