npm package: @quartal/bridge-client
Description: Universal client library for embedding applications with URL-configurable transport support (iframe, postMessage) and framework adapters for Angular and Vue
Author: Quartal Team
Latest version: 1.0.5

# 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