@signalwire/web-components 4.0.0-beta.12 → 4.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/UI/controls/sw-ui-dialpad.d.ts +1 -1
- package/dist/components/UI/controls/sw-ui-dialpad.d.ts.map +1 -1
- package/dist/components/UI/controls/sw-ui-dialpad.js +9 -8
- package/dist/components/UI/controls/sw-ui-dialpad.js.map +1 -1
- package/dist/components/UI/controls/sw-ui-dropup.d.ts +1 -1
- package/dist/components/UI/controls/sw-ui-dropup.d.ts.map +1 -1
- package/dist/components/UI/controls/sw-ui-dropup.js +9 -8
- package/dist/components/UI/controls/sw-ui-dropup.js.map +1 -1
- package/dist/components/UI/controls/sw-ui-split-button.d.ts +1 -1
- package/dist/components/UI/controls/sw-ui-split-button.d.ts.map +1 -1
- package/dist/components/UI/controls/sw-ui-split-button.js +13 -12
- package/dist/components/UI/controls/sw-ui-split-button.js.map +1 -1
- package/dist/components/UI/host-reset.d.ts +16 -0
- package/dist/components/UI/host-reset.d.ts.map +1 -0
- package/dist/components/UI/host-reset.js +20 -0
- package/dist/components/UI/host-reset.js.map +1 -0
- package/dist/components/UI/sw-ui-alert.d.ts +1 -1
- package/dist/components/UI/sw-ui-alert.d.ts.map +1 -1
- package/dist/components/UI/sw-ui-alert.js +14 -13
- package/dist/components/UI/sw-ui-alert.js.map +1 -1
- package/dist/components/UI/sw-ui-transcript-view.d.ts +1 -1
- package/dist/components/UI/sw-ui-transcript-view.d.ts.map +1 -1
- package/dist/components/UI/sw-ui-transcript-view.js +6 -5
- package/dist/components/UI/sw-ui-transcript-view.js.map +1 -1
- package/dist/components/sw-call-media.d.ts +0 -9
- package/dist/components/sw-call-media.d.ts.map +1 -1
- package/dist/components/sw-call-media.js +60 -102
- package/dist/components/sw-call-media.js.map +1 -1
- package/dist/components/sw-call-status.d.ts +1 -1
- package/dist/components/sw-call-status.d.ts.map +1 -1
- package/dist/components/sw-call-status.js +6 -5
- package/dist/components/sw-call-status.js.map +1 -1
- package/dist/components/sw-call-widget/sw-call-widget.d.ts +1 -1
- package/dist/components/sw-call-widget/sw-call-widget.d.ts.map +1 -1
- package/dist/components/sw-call-widget/sw-call-widget.js +15 -14
- package/dist/components/sw-call-widget/sw-call-widget.js.map +1 -1
- package/dist/components/sw-click-to-call.d.ts +1 -1
- package/dist/components/sw-click-to-call.d.ts.map +1 -1
- package/dist/components/sw-click-to-call.js +9 -8
- package/dist/components/sw-click-to-call.js.map +1 -1
- package/dist/components/sw-device-selector/sw-device-selector.d.ts +1 -1
- package/dist/components/sw-device-selector/sw-device-selector.d.ts.map +1 -1
- package/dist/components/sw-device-selector/sw-device-selector.js +5 -4
- package/dist/components/sw-device-selector/sw-device-selector.js.map +1 -1
- package/dist/components/sw-directory.d.ts +1 -1
- package/dist/components/sw-directory.d.ts.map +1 -1
- package/dist/components/sw-directory.js +6 -5
- package/dist/components/sw-directory.js.map +1 -1
- package/dist/components/sw-participant-controls.d.ts +1 -1
- package/dist/components/sw-participant-controls.d.ts.map +1 -1
- package/dist/components/sw-participant-controls.js +6 -5
- package/dist/components/sw-participant-controls.js.map +1 -1
- package/dist/components/sw-participants.d.ts +1 -1
- package/dist/components/sw-participants.d.ts.map +1 -1
- package/dist/components/sw-participants.js +20 -19
- package/dist/components/sw-participants.js.map +1 -1
- package/dist/embed/signalwire-web-components-embed.iife.js +195 -182
- package/dist/embed/signalwire-web-components-embed.iife.js.map +1 -1
- package/dist/embed/signalwire-web-components-embed.umd.cjs +195 -182
- package/dist/embed/signalwire-web-components-embed.umd.cjs.map +1 -1
- package/dist/index.js +46 -46
- package/dist/node_modules/dompurify/dist/purify.es.js +692 -423
- package/dist/node_modules/dompurify/dist/purify.es.js.map +1 -1
- package/dist/utils/video.js +6 -21
- package/dist/utils/video.js.map +1 -1
- package/package.json +5 -4
- package/dist/constants.js +0 -5
- package/dist/constants.js.map +0 -1
- package/dist/utils/debounce.js +0 -13
- package/dist/utils/debounce.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw-directory.js","sources":["../../src/components/sw-directory.ts"],"sourcesContent":["/**\n * Directory Component\n *\n * A searchable list of addresses from the directory service.\n * Supports filtering, selection, and pagination.\n *\n * @example\n * ```html\n * <sw-directory .directory=${directory}></sw-directory>\n * ```\n *\n * @fires sw-address-select - Fired when an address is selected. Detail: `{ address: Address }`\n * @fires sw-dial - Fired when the call button on an address is clicked. Detail: `{ address: Address, channel: string }`\n *\n * @cssprop [--interactive-button-primary-bg=#044ef4] - Primary brand color\n * @cssprop [--interactive-button-primary-hover=#0342cf] - Primary color on hover\n * @cssprop [--interactive-status-success=#22c55e] - Success/positive color\n * @cssprop [--fg-default=#f0f0f4] - Primary text color\n * @cssprop [--fg-muted=#a0a0aa] - Secondary/muted text color\n * @cssprop [--bg-surface=#181a28] - Component background color\n * @cssprop [--bg-surface-raised=#222436] - Background color on hover\n * @cssprop [--interactive-dropdown-hover=#333338] - Background color on active/press\n * @cssprop [--border-default=rgba(255,255,255,0.12)] - Border color\n */\n\nimport { LitElement, html, css } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { Subscription } from 'rxjs';\nimport type { Observable } from 'rxjs';\nimport './UI/icons/sw-ui-icon.js';\n\n/**\n * Address type from SDK\n */\nexport interface Address {\n id: string;\n name: string;\n displayName?: string;\n type?: 'room' | 'person';\n channels?: {\n audio?: boolean;\n video?: boolean;\n messaging?: boolean;\n };\n}\n\n/**\n * Directory interface for component\n */\nexport interface DirectoryService {\n addresses$: Observable<Address[]>;\n loading$?: Observable<boolean>;\n hasMore$?: Observable<boolean>;\n loadMore?(): Promise<void>;\n}\n\n@customElement('sw-directory')\nexport class SwDirectory extends LitElement {\n static styles = css`\n :host {\n display: block;\n font-family: var(--type-family-body);\n color: var(--fg-default);\n }\n\n .container {\n display: flex;\n flex-direction: column;\n background: var(--bg-surface);\n border: 1px solid var(--border-default);\n border-radius: var(--radius-md);\n overflow: hidden;\n }\n\n .search {\n display: flex;\n align-items: center;\n gap: var(--sp-2);\n padding: var(--sp-3);\n border-bottom: 1px solid var(--border-default);\n }\n\n .search-input {\n flex: 1;\n padding: var(--sp-2) var(--sp-3);\n border: 1px solid var(--border-default);\n border-radius: var(--radius-md);\n background: var(--bg-surface);\n color: var(--fg-default);\n font-family: var(--type-family-body);\n font-size: var(--type-size-small);\n outline: none;\n transition: border-color 0.15s ease;\n }\n\n .search-input:focus {\n border-color: var(--interactive-button-primary-bg);\n }\n\n .search-input::placeholder {\n color: var(--fg-muted);\n }\n\n .list {\n display: flex;\n flex-direction: column;\n max-height: 400px;\n overflow-y: auto;\n }\n\n .item {\n display: flex;\n align-items: center;\n gap: var(--sp-3);\n padding: var(--sp-3) var(--sp-4);\n border-bottom: 1px solid var(--border-default);\n cursor: pointer;\n transition: background-color 0.15s ease;\n }\n\n .item:last-child {\n border-bottom: none;\n }\n\n .item:hover {\n background: var(--bg-surface-raised);\n }\n\n .item:active {\n background: var(--interactive-dropdown-hover);\n }\n\n .item.selected {\n background: var(--interactive-button-primary-bg);\n color: white;\n }\n\n .item.selected .item-type,\n .item.selected .item-channels {\n color: rgba(255, 255, 255, 0.8);\n }\n\n .item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: var(--bg-surface-raised);\n color: var(--fg-muted);\n flex-shrink: 0;\n }\n\n .item.selected .item-icon {\n background: rgba(255, 255, 255, 0.2);\n color: white;\n }\n\n .item-icon svg,\n .item-icon sw-ui-icon {\n width: 20px;\n height: 20px;\n }\n\n .item-content {\n flex: 1;\n min-width: 0;\n }\n\n .item-name {\n font-size: var(--type-size-small);\n font-weight: 500;\n color: inherit;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .item-type {\n font-size: var(--type-size-caption);\n color: var(--fg-muted);\n text-transform: capitalize;\n }\n\n .item-channels {\n display: flex;\n gap: var(--sp-1);\n color: var(--fg-muted);\n }\n\n .item-channels sw-ui-icon {\n width: 16px;\n height: 16px;\n }\n\n .item-channels sw-ui-icon.active {\n color: var(--interactive-status-success);\n }\n\n .loading,\n .empty {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--sp-4);\n color: var(--fg-muted);\n font-size: var(--type-size-small);\n }\n\n .scroll-sentinel {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--sp-3);\n min-height: 1px;\n }\n\n .scroll-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--sp-2);\n padding: var(--sp-3);\n color: var(--fg-muted);\n font-size: var(--type-size-caption);\n }\n\n .spinner {\n width: 16px;\n height: 16px;\n border: 2px solid var(--border-default);\n border-top-color: var(--interactive-button-primary-bg);\n border-radius: 50%;\n animation: spin 0.8s linear infinite;\n }\n\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n\n .dial-button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: var(--interactive-status-success);\n border: none;\n border-radius: 50%;\n color: white;\n cursor: pointer;\n flex-shrink: 0;\n transition: background-color 0.15s ease;\n }\n\n .dial-button:hover {\n background: #0ea472;\n }\n\n .dial-button sw-ui-icon {\n pointer-events: none;\n }\n `;\n\n /**\n * Directory service with addresses$ observable\n */\n @property({ type: Object })\n directory: DirectoryService | null = null;\n\n /**\n * Currently selected address\n */\n @state()\n private selectedAddress: Address | null = null;\n\n /**\n * Search filter query\n */\n @state()\n private searchQuery: string = '';\n\n /**\n * List of addresses from directory\n */\n @state()\n private addresses: Address[] = [];\n\n /**\n * Loading state\n */\n @state()\n private loading: boolean = false;\n\n /**\n * Has more addresses to load\n */\n @state()\n private hasMore: boolean = false;\n\n /**\n * RxJS subscriptions for cleanup\n */\n private subscriptions: Subscription[] = [];\n\n /**\n * Debounce timer for search\n */\n private searchDebounceTimer: number | null = null;\n\n /**\n * IntersectionObserver for infinite scroll\n */\n private intersectionObserver: IntersectionObserver | null = null;\n\n /**\n * Flag to track if we're auto-loading for search\n */\n @state()\n private isAutoLoadingForSearch: boolean = false;\n\n connectedCallback() {\n super.connectedCallback();\n this.subscribeToDirectory();\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.cleanup();\n }\n\n private subscribeToDirectory() {\n if (!this.directory) return;\n\n // Subscribe to addresses\n if (this.directory.addresses$) {\n const addressesSub = this.directory.addresses$.subscribe((addresses) => {\n this.addresses = addresses;\n });\n this.subscriptions.push(addressesSub);\n }\n\n // Subscribe to loading state\n if (this.directory.loading$) {\n const loadingSub = this.directory.loading$.subscribe((loading) => {\n this.loading = loading;\n });\n this.subscriptions.push(loadingSub);\n }\n\n // Subscribe to hasMore\n if (this.directory.hasMore$) {\n const hasMoreSub = this.directory.hasMore$.subscribe((hasMore) => {\n this.hasMore = hasMore;\n });\n this.subscriptions.push(hasMoreSub);\n }\n\n // Load initial data\n if (this.directory.loadMore) {\n this.directory.loadMore();\n }\n }\n\n private cleanup() {\n this.subscriptions.forEach((sub) => sub.unsubscribe());\n this.subscriptions = [];\n if (this.searchDebounceTimer) {\n clearTimeout(this.searchDebounceTimer);\n }\n if (this.intersectionObserver) {\n this.intersectionObserver.disconnect();\n this.intersectionObserver = null;\n }\n }\n\n protected firstUpdated() {\n this.setupInfiniteScroll();\n }\n\n private setupInfiniteScroll() {\n const sentinel = this.shadowRoot?.querySelector('.scroll-sentinel');\n if (!sentinel) return;\n\n this.intersectionObserver = new IntersectionObserver(\n (entries) => {\n const entry = entries[0];\n if (entry?.isIntersecting && this.hasMore && !this.loading) {\n this.handleLoadMore();\n }\n },\n {\n root: this.shadowRoot?.querySelector('.list'),\n rootMargin: '100px',\n threshold: 0\n }\n );\n\n this.intersectionObserver.observe(sentinel);\n }\n\n protected updated(changedProperties: Map<string, unknown>) {\n super.updated(changedProperties);\n\n // Handle directory property changes\n if (changedProperties.has('directory')) {\n this.cleanup();\n this.subscribeToDirectory();\n }\n\n // Re-setup infinite scroll if needed\n if (!this.intersectionObserver) {\n this.setupInfiniteScroll();\n }\n\n // Auto-load more when searching and no results found but more available\n // Also check when loading state changes so we can trigger subsequent loads\n if (\n changedProperties.has('addresses') ||\n changedProperties.has('searchQuery') ||\n changedProperties.has('loading')\n ) {\n this.checkAutoLoadForSearch();\n }\n }\n\n private async checkAutoLoadForSearch() {\n // If we have a search query, no filtered results, but more to load, keep loading\n // Note: !this.loading prevents concurrent loads, isAutoLoadingForSearch is for UI state only\n if (\n this.searchQuery.trim() &&\n this.filteredAddresses.length === 0 &&\n this.hasMore &&\n !this.loading\n ) {\n this.isAutoLoadingForSearch = true;\n await this.handleLoadMore();\n // The updated() will be called again when addresses/loading change, continuing the loop\n } else if (this.filteredAddresses.length > 0 || !this.hasMore) {\n this.isAutoLoadingForSearch = false;\n }\n }\n\n private get filteredAddresses(): Address[] {\n if (!this.searchQuery.trim()) {\n return this.addresses;\n }\n const query = this.searchQuery.toLowerCase();\n return this.addresses.filter(\n (addr) =>\n addr.name.toLowerCase().includes(query) ||\n (addr.displayName && addr.displayName.toLowerCase().includes(query))\n );\n }\n\n private handleSearchInput(e: Event) {\n const input = e.target as HTMLInputElement;\n\n // Debounce search\n if (this.searchDebounceTimer) {\n clearTimeout(this.searchDebounceTimer);\n }\n\n this.searchDebounceTimer = window.setTimeout(() => {\n this.searchQuery = input.value;\n }, 200);\n }\n\n private handleItemClick(address: Address) {\n this.selectedAddress = address;\n\n this.dispatchEvent(\n new CustomEvent('sw-address-select', {\n detail: { address },\n bubbles: true,\n composed: true\n })\n );\n }\n\n private handleItemDoubleClick(address: Address) {\n this.handleDial(address);\n }\n\n private handleDial(address: Address) {\n this.dispatchEvent(\n new CustomEvent('sw-dial', {\n detail: { address },\n bubbles: true,\n composed: true\n })\n );\n }\n\n private async handleLoadMore() {\n if (this.directory?.loadMore) {\n await this.directory.loadMore();\n }\n }\n\n private renderAddressIcon(address: Address) {\n const name = address.type === 'room' ? 'room' : 'person';\n return html`<sw-ui-icon name=${name} size=\"20\"></sw-ui-icon>`;\n }\n\n private renderChannelIcons(address: Address) {\n const channels = address.channels || {};\n return html`\n <div class=\"item-channels\">\n <sw-ui-icon\n name=\"mic-on\"\n size=\"16\"\n class=\"${channels.audio ? 'active' : ''}\"\n title=\"Audio\"\n ></sw-ui-icon>\n <sw-ui-icon\n name=\"camera-on\"\n size=\"16\"\n class=\"${channels.video ? 'active' : ''}\"\n title=\"Video\"\n ></sw-ui-icon>\n </div>\n `;\n }\n\n private renderLoadingIndicator() {\n return html`\n <div class=\"scroll-loading\">\n <div class=\"spinner\"></div>\n <span>${this.isAutoLoadingForSearch ? 'Searching...' : 'Loading more...'}</span>\n </div>\n `;\n }\n\n render() {\n const showInitialLoading = this.loading && this.addresses.length === 0;\n const showEmptyState = !this.loading && this.filteredAddresses.length === 0 && !this.hasMore;\n const showSearchingState = this.isAutoLoadingForSearch && this.filteredAddresses.length === 0;\n\n return html`\n <div class=\"container\" part=\"container\">\n <div class=\"search\" part=\"search\">\n <input\n type=\"text\"\n class=\"search-input\"\n placeholder=\"Search addresses...\"\n @input=${this.handleSearchInput}\n aria-label=\"Search addresses\"\n />\n </div>\n\n <div class=\"list\" part=\"list\" role=\"listbox\">\n ${showInitialLoading\n ? html`<div class=\"loading\">Loading...</div>`\n : showSearchingState\n ? this.renderLoadingIndicator()\n : showEmptyState\n ? html`<div class=\"empty\">No addresses found</div>`\n : html`\n ${this.filteredAddresses.map(\n (address) => html`\n <div\n class=\"item ${this.selectedAddress?.id === address.id ? 'selected' : ''}\"\n part=\"item ${this.selectedAddress?.id === address.id\n ? 'item-selected'\n : ''}\"\n role=\"option\"\n aria-selected=\"${this.selectedAddress?.id === address.id}\"\n @click=${() => this.handleItemClick(address)}\n @dblclick=${() => this.handleItemDoubleClick(address)}\n >\n <div class=\"item-icon\">${this.renderAddressIcon(address)}</div>\n <div class=\"item-content\">\n <div class=\"item-name\" part=\"item-name\">\n ${address.displayName || address.name}\n </div>\n <div class=\"item-type\" part=\"item-type\">\n ${address.type || 'address'}\n </div>\n </div>\n ${this.renderChannelIcons(address)}\n <button\n class=\"dial-button\"\n @click=${(e: Event) => {\n e.stopPropagation();\n this.handleDial(address);\n }}\n aria-label=\"Call ${address.displayName || address.name}\"\n >\n <sw-ui-icon name=\"phone-call\" size=\"18\"></sw-ui-icon>\n </button>\n </div>\n `\n )}\n <!-- Scroll sentinel for infinite scroll -->\n <div class=\"scroll-sentinel\">\n ${this.loading && !this.isAutoLoadingForSearch\n ? this.renderLoadingIndicator()\n : null}\n </div>\n `}\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sw-directory': SwDirectory;\n }\n}\n"],"names":["SwDirectory","LitElement","addressesSub","addresses","loadingSub","loading","hasMoreSub","hasMore","sub","sentinel","_a","entries","entry","_b","changedProperties","query","addr","input","address","name","html","channels","showInitialLoading","showEmptyState","showSearchingState","_c","e","css","__decorateClass","property","state","customElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sw-directory.js","sources":["../../src/components/sw-directory.ts"],"sourcesContent":["/**\n * Directory Component\n *\n * A searchable list of addresses from the directory service.\n * Supports filtering, selection, and pagination.\n *\n * @example\n * ```html\n * <sw-directory .directory=${directory}></sw-directory>\n * ```\n *\n * @fires sw-address-select - Fired when an address is selected. Detail: `{ address: Address }`\n * @fires sw-dial - Fired when the call button on an address is clicked. Detail: `{ address: Address, channel: string }`\n *\n * @cssprop [--interactive-button-primary-bg=#044ef4] - Primary brand color\n * @cssprop [--interactive-button-primary-hover=#0342cf] - Primary color on hover\n * @cssprop [--interactive-status-success=#22c55e] - Success/positive color\n * @cssprop [--fg-default=#f0f0f4] - Primary text color\n * @cssprop [--fg-muted=#a0a0aa] - Secondary/muted text color\n * @cssprop [--bg-surface=#181a28] - Component background color\n * @cssprop [--bg-surface-raised=#222436] - Background color on hover\n * @cssprop [--interactive-dropdown-hover=#333338] - Background color on active/press\n * @cssprop [--border-default=rgba(255,255,255,0.12)] - Border color\n */\n\nimport { LitElement, html, css } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { Subscription } from 'rxjs';\nimport type { Observable } from 'rxjs';\nimport './UI/icons/sw-ui-icon.js';\nimport { hostReset } from './UI/host-reset.js';\n\n/**\n * Address type from SDK\n */\nexport interface Address {\n id: string;\n name: string;\n displayName?: string;\n type?: 'room' | 'person';\n channels?: {\n audio?: boolean;\n video?: boolean;\n messaging?: boolean;\n };\n}\n\n/**\n * Directory interface for component\n */\nexport interface DirectoryService {\n addresses$: Observable<Address[]>;\n loading$?: Observable<boolean>;\n hasMore$?: Observable<boolean>;\n loadMore?(): Promise<void>;\n}\n\n@customElement('sw-directory')\nexport class SwDirectory extends LitElement {\n static styles = [hostReset, css`\n :host {\n display: block;\n font-family: var(--type-family-body);\n color: var(--fg-default);\n }\n\n .container {\n display: flex;\n flex-direction: column;\n background: var(--bg-surface);\n border: 1px solid var(--border-default);\n border-radius: var(--radius-md);\n overflow: hidden;\n }\n\n .search {\n display: flex;\n align-items: center;\n gap: var(--sp-2);\n padding: var(--sp-3);\n border-bottom: 1px solid var(--border-default);\n }\n\n .search-input {\n flex: 1;\n padding: var(--sp-2) var(--sp-3);\n border: 1px solid var(--border-default);\n border-radius: var(--radius-md);\n background: var(--bg-surface);\n color: var(--fg-default);\n font-family: var(--type-family-body);\n font-size: var(--type-size-small);\n outline: none;\n transition: border-color 0.15s ease;\n }\n\n .search-input:focus {\n border-color: var(--interactive-button-primary-bg);\n }\n\n .search-input::placeholder {\n color: var(--fg-muted);\n }\n\n .list {\n display: flex;\n flex-direction: column;\n max-height: 400px;\n overflow-y: auto;\n }\n\n .item {\n display: flex;\n align-items: center;\n gap: var(--sp-3);\n padding: var(--sp-3) var(--sp-4);\n border-bottom: 1px solid var(--border-default);\n cursor: pointer;\n transition: background-color 0.15s ease;\n }\n\n .item:last-child {\n border-bottom: none;\n }\n\n .item:hover {\n background: var(--bg-surface-raised);\n }\n\n .item:active {\n background: var(--interactive-dropdown-hover);\n }\n\n .item.selected {\n background: var(--interactive-button-primary-bg);\n color: white;\n }\n\n .item.selected .item-type,\n .item.selected .item-channels {\n color: rgba(255, 255, 255, 0.8);\n }\n\n .item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: var(--bg-surface-raised);\n color: var(--fg-muted);\n flex-shrink: 0;\n }\n\n .item.selected .item-icon {\n background: rgba(255, 255, 255, 0.2);\n color: white;\n }\n\n .item-icon svg,\n .item-icon sw-ui-icon {\n width: 20px;\n height: 20px;\n }\n\n .item-content {\n flex: 1;\n min-width: 0;\n }\n\n .item-name {\n font-size: var(--type-size-small);\n font-weight: 500;\n color: inherit;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .item-type {\n font-size: var(--type-size-caption);\n color: var(--fg-muted);\n text-transform: capitalize;\n }\n\n .item-channels {\n display: flex;\n gap: var(--sp-1);\n color: var(--fg-muted);\n }\n\n .item-channels sw-ui-icon {\n width: 16px;\n height: 16px;\n }\n\n .item-channels sw-ui-icon.active {\n color: var(--interactive-status-success);\n }\n\n .loading,\n .empty {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--sp-4);\n color: var(--fg-muted);\n font-size: var(--type-size-small);\n }\n\n .scroll-sentinel {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--sp-3);\n min-height: 1px;\n }\n\n .scroll-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--sp-2);\n padding: var(--sp-3);\n color: var(--fg-muted);\n font-size: var(--type-size-caption);\n }\n\n .spinner {\n width: 16px;\n height: 16px;\n border: 2px solid var(--border-default);\n border-top-color: var(--interactive-button-primary-bg);\n border-radius: 50%;\n animation: spin 0.8s linear infinite;\n }\n\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n\n .dial-button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: var(--interactive-status-success);\n border: none;\n border-radius: 50%;\n color: white;\n cursor: pointer;\n flex-shrink: 0;\n transition: background-color 0.15s ease;\n }\n\n .dial-button:hover {\n background: #0ea472;\n }\n\n .dial-button sw-ui-icon {\n pointer-events: none;\n }\n `];\n\n /**\n * Directory service with addresses$ observable\n */\n @property({ type: Object })\n directory: DirectoryService | null = null;\n\n /**\n * Currently selected address\n */\n @state()\n private selectedAddress: Address | null = null;\n\n /**\n * Search filter query\n */\n @state()\n private searchQuery: string = '';\n\n /**\n * List of addresses from directory\n */\n @state()\n private addresses: Address[] = [];\n\n /**\n * Loading state\n */\n @state()\n private loading: boolean = false;\n\n /**\n * Has more addresses to load\n */\n @state()\n private hasMore: boolean = false;\n\n /**\n * RxJS subscriptions for cleanup\n */\n private subscriptions: Subscription[] = [];\n\n /**\n * Debounce timer for search\n */\n private searchDebounceTimer: number | null = null;\n\n /**\n * IntersectionObserver for infinite scroll\n */\n private intersectionObserver: IntersectionObserver | null = null;\n\n /**\n * Flag to track if we're auto-loading for search\n */\n @state()\n private isAutoLoadingForSearch: boolean = false;\n\n connectedCallback() {\n super.connectedCallback();\n this.subscribeToDirectory();\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.cleanup();\n }\n\n private subscribeToDirectory() {\n if (!this.directory) return;\n\n // Subscribe to addresses\n if (this.directory.addresses$) {\n const addressesSub = this.directory.addresses$.subscribe((addresses) => {\n this.addresses = addresses;\n });\n this.subscriptions.push(addressesSub);\n }\n\n // Subscribe to loading state\n if (this.directory.loading$) {\n const loadingSub = this.directory.loading$.subscribe((loading) => {\n this.loading = loading;\n });\n this.subscriptions.push(loadingSub);\n }\n\n // Subscribe to hasMore\n if (this.directory.hasMore$) {\n const hasMoreSub = this.directory.hasMore$.subscribe((hasMore) => {\n this.hasMore = hasMore;\n });\n this.subscriptions.push(hasMoreSub);\n }\n\n // Load initial data\n if (this.directory.loadMore) {\n this.directory.loadMore();\n }\n }\n\n private cleanup() {\n this.subscriptions.forEach((sub) => sub.unsubscribe());\n this.subscriptions = [];\n if (this.searchDebounceTimer) {\n clearTimeout(this.searchDebounceTimer);\n }\n if (this.intersectionObserver) {\n this.intersectionObserver.disconnect();\n this.intersectionObserver = null;\n }\n }\n\n protected firstUpdated() {\n this.setupInfiniteScroll();\n }\n\n private setupInfiniteScroll() {\n const sentinel = this.shadowRoot?.querySelector('.scroll-sentinel');\n if (!sentinel) return;\n\n this.intersectionObserver = new IntersectionObserver(\n (entries) => {\n const entry = entries[0];\n if (entry?.isIntersecting && this.hasMore && !this.loading) {\n this.handleLoadMore();\n }\n },\n {\n root: this.shadowRoot?.querySelector('.list'),\n rootMargin: '100px',\n threshold: 0\n }\n );\n\n this.intersectionObserver.observe(sentinel);\n }\n\n protected updated(changedProperties: Map<string, unknown>) {\n super.updated(changedProperties);\n\n // Handle directory property changes\n if (changedProperties.has('directory')) {\n this.cleanup();\n this.subscribeToDirectory();\n }\n\n // Re-setup infinite scroll if needed\n if (!this.intersectionObserver) {\n this.setupInfiniteScroll();\n }\n\n // Auto-load more when searching and no results found but more available\n // Also check when loading state changes so we can trigger subsequent loads\n if (\n changedProperties.has('addresses') ||\n changedProperties.has('searchQuery') ||\n changedProperties.has('loading')\n ) {\n this.checkAutoLoadForSearch();\n }\n }\n\n private async checkAutoLoadForSearch() {\n // If we have a search query, no filtered results, but more to load, keep loading\n // Note: !this.loading prevents concurrent loads, isAutoLoadingForSearch is for UI state only\n if (\n this.searchQuery.trim() &&\n this.filteredAddresses.length === 0 &&\n this.hasMore &&\n !this.loading\n ) {\n this.isAutoLoadingForSearch = true;\n await this.handleLoadMore();\n // The updated() will be called again when addresses/loading change, continuing the loop\n } else if (this.filteredAddresses.length > 0 || !this.hasMore) {\n this.isAutoLoadingForSearch = false;\n }\n }\n\n private get filteredAddresses(): Address[] {\n if (!this.searchQuery.trim()) {\n return this.addresses;\n }\n const query = this.searchQuery.toLowerCase();\n return this.addresses.filter(\n (addr) =>\n addr.name.toLowerCase().includes(query) ||\n (addr.displayName && addr.displayName.toLowerCase().includes(query))\n );\n }\n\n private handleSearchInput(e: Event) {\n const input = e.target as HTMLInputElement;\n\n // Debounce search\n if (this.searchDebounceTimer) {\n clearTimeout(this.searchDebounceTimer);\n }\n\n this.searchDebounceTimer = window.setTimeout(() => {\n this.searchQuery = input.value;\n }, 200);\n }\n\n private handleItemClick(address: Address) {\n this.selectedAddress = address;\n\n this.dispatchEvent(\n new CustomEvent('sw-address-select', {\n detail: { address },\n bubbles: true,\n composed: true\n })\n );\n }\n\n private handleItemDoubleClick(address: Address) {\n this.handleDial(address);\n }\n\n private handleDial(address: Address) {\n this.dispatchEvent(\n new CustomEvent('sw-dial', {\n detail: { address },\n bubbles: true,\n composed: true\n })\n );\n }\n\n private async handleLoadMore() {\n if (this.directory?.loadMore) {\n await this.directory.loadMore();\n }\n }\n\n private renderAddressIcon(address: Address) {\n const name = address.type === 'room' ? 'room' : 'person';\n return html`<sw-ui-icon name=${name} size=\"20\"></sw-ui-icon>`;\n }\n\n private renderChannelIcons(address: Address) {\n const channels = address.channels || {};\n return html`\n <div class=\"item-channels\">\n <sw-ui-icon\n name=\"mic-on\"\n size=\"16\"\n class=\"${channels.audio ? 'active' : ''}\"\n title=\"Audio\"\n ></sw-ui-icon>\n <sw-ui-icon\n name=\"camera-on\"\n size=\"16\"\n class=\"${channels.video ? 'active' : ''}\"\n title=\"Video\"\n ></sw-ui-icon>\n </div>\n `;\n }\n\n private renderLoadingIndicator() {\n return html`\n <div class=\"scroll-loading\">\n <div class=\"spinner\"></div>\n <span>${this.isAutoLoadingForSearch ? 'Searching...' : 'Loading more...'}</span>\n </div>\n `;\n }\n\n render() {\n const showInitialLoading = this.loading && this.addresses.length === 0;\n const showEmptyState = !this.loading && this.filteredAddresses.length === 0 && !this.hasMore;\n const showSearchingState = this.isAutoLoadingForSearch && this.filteredAddresses.length === 0;\n\n return html`\n <div class=\"container\" part=\"container\">\n <div class=\"search\" part=\"search\">\n <input\n type=\"text\"\n class=\"search-input\"\n placeholder=\"Search addresses...\"\n @input=${this.handleSearchInput}\n aria-label=\"Search addresses\"\n />\n </div>\n\n <div class=\"list\" part=\"list\" role=\"listbox\">\n ${showInitialLoading\n ? html`<div class=\"loading\">Loading...</div>`\n : showSearchingState\n ? this.renderLoadingIndicator()\n : showEmptyState\n ? html`<div class=\"empty\">No addresses found</div>`\n : html`\n ${this.filteredAddresses.map(\n (address) => html`\n <div\n class=\"item ${this.selectedAddress?.id === address.id ? 'selected' : ''}\"\n part=\"item ${this.selectedAddress?.id === address.id\n ? 'item-selected'\n : ''}\"\n role=\"option\"\n aria-selected=\"${this.selectedAddress?.id === address.id}\"\n @click=${() => this.handleItemClick(address)}\n @dblclick=${() => this.handleItemDoubleClick(address)}\n >\n <div class=\"item-icon\">${this.renderAddressIcon(address)}</div>\n <div class=\"item-content\">\n <div class=\"item-name\" part=\"item-name\">\n ${address.displayName || address.name}\n </div>\n <div class=\"item-type\" part=\"item-type\">\n ${address.type || 'address'}\n </div>\n </div>\n ${this.renderChannelIcons(address)}\n <button\n class=\"dial-button\"\n @click=${(e: Event) => {\n e.stopPropagation();\n this.handleDial(address);\n }}\n aria-label=\"Call ${address.displayName || address.name}\"\n >\n <sw-ui-icon name=\"phone-call\" size=\"18\"></sw-ui-icon>\n </button>\n </div>\n `\n )}\n <!-- Scroll sentinel for infinite scroll -->\n <div class=\"scroll-sentinel\">\n ${this.loading && !this.isAutoLoadingForSearch\n ? this.renderLoadingIndicator()\n : null}\n </div>\n `}\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sw-directory': SwDirectory;\n }\n}\n"],"names":["SwDirectory","LitElement","addressesSub","addresses","loadingSub","loading","hasMoreSub","hasMore","sub","sentinel","_a","entries","entry","_b","changedProperties","query","addr","input","address","name","html","channels","showInitialLoading","showEmptyState","showSearchingState","_c","e","hostReset","css","__decorateClass","property","state","customElement"],"mappings":"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"}
|
|
@@ -24,7 +24,7 @@ import { LitElement } from 'lit';
|
|
|
24
24
|
import type { Call } from '../types/index.js';
|
|
25
25
|
import './UI/icons/sw-ui-icon.js';
|
|
26
26
|
export declare class SwParticipantControls extends LitElement {
|
|
27
|
-
static styles: import("lit").CSSResult;
|
|
27
|
+
static styles: import("lit").CSSResult[];
|
|
28
28
|
participantId: string;
|
|
29
29
|
showVolume: boolean;
|
|
30
30
|
showPin: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw-participant-controls.d.ts","sourceRoot":"","sources":["../../src/components/sw-participant-controls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAMrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"sw-participant-controls.d.ts","sourceRoot":"","sources":["../../src/components/sw-participant-controls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAMrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,0BAA0B,CAAC;AAKlC,qBACa,qBAAsB,SAAQ,UAAU;IACnD,MAAM,CAAC,MAAM,4BAyHV;IAGH,aAAa,SAAM;IAGnB,UAAU,UAAS;IAGnB,OAAO,UAAS;IAIhB,OAAO,CAAC,UAAU,CAAC,CAAY;IAE/B,0EAA0E;IAC9C,IAAI,CAAC,EAAE,IAAI,CAAC;IAE/B,OAAO,CAAC,mBAAmB,CAAyB;IACpD,OAAO,CAAC,mBAAmB,CAAgB;IAEpD,OAAO,CAAC,oBAAoB,CAAsB;IAEzC,OAAO,CAAC,OAAO,CAAO;IACtB,OAAO,CAAC,SAAS,CAAS;IAEnC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAgBtD,oBAAoB;IAQpB,OAAO,KAAK,YAAY,GAGvB;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,KAAK,UAAU,GAErB;YAIa,gBAAgB;YAchB,gBAAgB;YAchB,OAAO;IAUrB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,UAAU;IAalB,MAAM;CAsEP;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,yBAAyB,EAAE,qBAAqB,CAAC;KAClD;CACF"}
|
|
@@ -3,10 +3,11 @@ import { property as l, state as c, customElement as v } from "lit/decorators.js
|
|
|
3
3
|
import { consume as g } from "@lit/context";
|
|
4
4
|
import { callStateContext as f } from "../context/call-state-context.js";
|
|
5
5
|
import { getLogger as _ } from "@signalwire/js";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { hostReset as w } from "./UI/host-reset.js";
|
|
7
|
+
var y = Object.defineProperty, x = Object.getOwnPropertyDescriptor, a = (t, i, n, p) => {
|
|
8
|
+
for (var o = p > 1 ? void 0 : p ? x(i, n) : i, u = t.length - 1, d; u >= 0; u--)
|
|
8
9
|
(d = t[u]) && (o = (p ? d(i, n, o) : d(o)) || o);
|
|
9
|
-
return p && o &&
|
|
10
|
+
return p && o && y(i, n, o), o;
|
|
10
11
|
};
|
|
11
12
|
const h = _();
|
|
12
13
|
let e = class extends b {
|
|
@@ -146,7 +147,7 @@ let e = class extends b {
|
|
|
146
147
|
`;
|
|
147
148
|
}
|
|
148
149
|
};
|
|
149
|
-
e.styles = m`
|
|
150
|
+
e.styles = [w, m`
|
|
150
151
|
:host {
|
|
151
152
|
display: block;
|
|
152
153
|
font-family: var(--type-family-body);
|
|
@@ -267,7 +268,7 @@ e.styles = m`
|
|
|
267
268
|
text-align: center;
|
|
268
269
|
padding: 6px;
|
|
269
270
|
}
|
|
270
|
-
|
|
271
|
+
`];
|
|
271
272
|
a([
|
|
272
273
|
l({ type: String, attribute: "participant-id" })
|
|
273
274
|
], e.prototype, "participantId", 2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw-participant-controls.js","sources":["../../src/components/sw-participant-controls.ts"],"sourcesContent":["/**\n * Per-participant control panel.\n *\n * Takes a `participant-id` and looks up the participant.\n * Shows mute audio, mute video, volume, pin, and remove actions based on\n * the `capabilities` list from the call state.\n *\n * Input precedence (most specific wins): `.call` > context.\n *\n * @prop {string} participantId - ID of the participant to control\n * @prop {Call} call - explicit Call object (overrides context)\n * @prop {boolean} showVolume - show volume slider\n * @prop {boolean} showPin - show pin/spotlight button\n *\n * @fires sw-participant-volume-change - Volume slider changed. Detail: `{ participantId: string, volume: number }`.\n * @fires sw-participant-pin-toggle - Pin/unpin clicked. Detail: `{ participantId: string, pinned: boolean }`.\n *\n * Themed via the SignalWire DTCG tokens (`--bg-page`, `--bg-surface`, `--bg-surface-raised`,\n * `--fg-default`, `--border-default`, `--radius-md`, `--shadow-md`, `--type-family-body`,\n * `--type-size-small`, `--interactive-button-destructive-bg`,\n * `--interactive-button-destructive-hover`, `--transition-fast`).\n */\n\nimport { LitElement, html, css, nothing } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { consume } from '@lit/context';\nimport { Subscription } from 'rxjs';\nimport type { CallParticipant } from '@signalwire/js';\nimport { callStateContext, type CallState } from '../context/call-state-context.js';\nimport type { Call } from '../types/index.js';\nimport { getLogger } from '@signalwire/js';\nimport './UI/icons/sw-ui-icon.js';\n\nconst logger = getLogger();\n\n@customElement('sw-participant-controls')\nexport class SwParticipantControls extends LitElement {\n static styles = css`\n :host {\n display: block;\n font-family: var(--type-family-body);\n font-size: var(--type-size-small);\n }\n\n .panel {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px;\n background: var(--bg-page);\n border: 1px solid var(--border-default);\n border-radius: var(--radius-md);\n min-width: 180px;\n box-shadow: var(--shadow-md);\n color: var(--fg-default);\n }\n\n .name {\n font-size: 0.8125rem;\n font-weight: 500;\n padding-bottom: 6px;\n border-bottom: 1px solid var(--border-default);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .actions {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n button {\n all: unset;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 10px;\n border-radius: var(--radius-md);\n font-size: 0.75rem;\n cursor: pointer;\n background: var(--bg-surface);\n color: var(--fg-default);\n transition: background var(--transition-fast);\n }\n\n button:hover {\n background: var(--bg-surface-raised);\n }\n\n button.active {\n background: var(--interactive-button-destructive-bg);\n color: #fff;\n }\n\n button.active:hover {\n background: var(--interactive-button-destructive-hover);\n }\n\n button.danger {\n color: var(--interactive-button-destructive-bg);\n }\n\n button.danger:hover {\n background: var(--interactive-button-destructive-bg);\n color: #fff;\n }\n\n .volume-control {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 4px 0;\n }\n\n .volume-label {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 0.7rem;\n color: color-mix(in srgb, var(--fg-default) 50%, transparent);\n }\n\n input[type='range'] {\n width: 100%;\n height: 4px;\n appearance: none;\n background: var(--border-default);\n border-radius: 2px;\n outline: none;\n cursor: pointer;\n }\n\n input[type='range']::-webkit-slider-thumb {\n appearance: none;\n width: 12px;\n height: 12px;\n background: #3b82f6;\n border-radius: 50%;\n cursor: pointer;\n }\n\n input[type='range']::-moz-range-thumb {\n width: 12px;\n height: 12px;\n background: #3b82f6;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n }\n\n .empty {\n font-size: 0.7rem;\n color: color-mix(in srgb, var(--fg-default) 50%, transparent);\n text-align: center;\n padding: 6px;\n }\n `;\n\n @property({ type: String, attribute: 'participant-id' })\n participantId = '';\n\n @property({ type: Boolean, reflect: true, attribute: 'show-volume' })\n showVolume = false;\n\n @property({ type: Boolean, reflect: true, attribute: 'show-pin' })\n showPin = false;\n\n @consume({ context: callStateContext, subscribe: true })\n @state()\n private _callState?: CallState;\n\n /** Explicit Call — when set, subscribes directly and bypasses context. */\n @property({ type: Object }) call?: Call;\n\n @state() private _directParticipants: CallParticipant[] = [];\n @state() private _directCapabilities: string[] = [];\n\n private _directSubscriptions: Subscription[] = [];\n\n @state() private _volume = 100;\n @state() private _isPinned = false;\n\n protected updated(changed: Map<string, unknown>): void {\n super.updated(changed);\n if (changed.has('call')) {\n this._directSubscriptions.forEach((s) => s.unsubscribe());\n this._directSubscriptions = [];\n this._directParticipants = [];\n this._directCapabilities = [];\n if (this.call) {\n this._directSubscriptions.push(\n this.call.participants$.subscribe((p) => (this._directParticipants = p)),\n this.call.capabilities$.subscribe((c) => (this._directCapabilities = c))\n );\n }\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this._directSubscriptions.forEach((s) => s.unsubscribe());\n this._directSubscriptions = [];\n }\n\n // ── Derived state ──────────────────────────────────────────────────\n\n private get _participant() {\n const participants = this.call ? this._directParticipants : this._callState?.participants ?? [];\n return participants.find((p) => p.id === this.participantId);\n }\n\n private get _capabilities(): string[] {\n return this.call ? this._directCapabilities : this._callState?.capabilities ?? [];\n }\n\n private get _canMuteAudio(): boolean {\n return this._capabilities.includes('memberMuteAudio');\n }\n\n private get _canMuteVideo(): boolean {\n return this._capabilities.includes('memberMuteVideo');\n }\n\n private get _canRemove(): boolean {\n return this._capabilities.includes('memberRemove');\n }\n\n // ── Actions ────────────────────────────────────────────────────────\n\n private async _toggleAudioMute() {\n const p = this._participant;\n if (!p) return;\n try {\n if (p.audioMuted) {\n await p.unmute();\n } else {\n await p.mute();\n }\n } catch (err) {\n logger.error('[ParticipantControls] Toggle audio failed:', err);\n }\n }\n\n private async _toggleVideoMute() {\n const p = this._participant;\n if (!p) return;\n try {\n if (p.videoMuted) {\n await p.unmuteVideo();\n } else {\n await p.muteVideo();\n }\n } catch (err) {\n logger.error('[ParticipantControls] Toggle video failed:', err);\n }\n }\n\n private async _remove() {\n const p = this._participant;\n if (!p?.remove) return;\n try {\n await p.remove();\n } catch (err) {\n logger.error('[ParticipantControls] Remove failed:', err);\n }\n }\n\n private _onVolumeChange(e: Event) {\n this._volume = parseInt((e.target as HTMLInputElement).value, 10);\n this.dispatchEvent(\n new CustomEvent('sw-participant-volume-change', {\n detail: { participantId: this.participantId, volume: this._volume },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n private _togglePin() {\n this._isPinned = !this._isPinned;\n this.dispatchEvent(\n new CustomEvent('sw-participant-pin-toggle', {\n detail: { participantId: this.participantId, pinned: this._isPinned },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n // ── Render ─────────────────────────────────────────────────────────\n\n render() {\n const p = this._participant;\n if (!p) return html`<div class=\"panel\"><div class=\"empty\">Participant not found</div></div>`;\n\n const hasActions = this._canMuteAudio || this._canMuteVideo || this._canRemove || this.showVolume || this.showPin;\n\n return html`\n <div class=\"panel\" part=\"panel\">\n <div class=\"name\" part=\"name\">${p.name ?? 'Participant'}</div>\n\n <div class=\"actions\">\n ${!hasActions ? html`<div class=\"empty\">No actions available</div>` : nothing}\n\n ${this._canMuteAudio\n ? html`\n <button class=${p.audioMuted ? 'active' : ''} @click=${this._toggleAudioMute}>\n <sw-ui-icon name=${p.audioMuted ? 'mic-off' : 'mic-on'} size=\"14\"></sw-ui-icon>\n ${p.audioMuted ? 'Unmute' : 'Mute'}\n </button>\n `\n : nothing}\n\n ${this._canMuteVideo\n ? html`\n <button class=${p.videoMuted ? 'active' : ''} @click=${this._toggleVideoMute}>\n <sw-ui-icon name=${p.videoMuted ? 'camera-off' : 'camera-on'} size=\"14\"></sw-ui-icon>\n ${p.videoMuted ? 'Enable video' : 'Disable video'}\n </button>\n `\n : nothing}\n\n ${this.showVolume\n ? html`\n <div class=\"volume-control\">\n <label class=\"volume-label\">\n <sw-ui-icon name=\"speaker-on\" size=\"14\"></sw-ui-icon>\n Volume: ${this._volume}%\n </label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"100\"\n .value=${String(this._volume)}\n @input=${this._onVolumeChange}\n />\n </div>\n `\n : nothing}\n\n ${this.showPin\n ? html`\n <button class=${this._isPinned ? 'active' : ''} @click=${this._togglePin}>\n <sw-ui-icon name=\"settings\" size=\"14\"></sw-ui-icon>\n ${this._isPinned ? 'Unpin' : 'Pin'}\n </button>\n `\n : nothing}\n\n ${this._canRemove\n ? html`\n <button class=\"danger\" @click=${this._remove}>\n <sw-ui-icon name=\"close\" size=\"14\"></sw-ui-icon>\n Remove\n </button>\n `\n : nothing}\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sw-participant-controls': SwParticipantControls;\n }\n}\n"],"names":["logger","getLogger","SwParticipantControls","LitElement","changed","s","p","c","_a","err","e","html","hasActions","nothing","css","__decorateClass","property","consume","callStateContext","state","customElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sw-participant-controls.js","sources":["../../src/components/sw-participant-controls.ts"],"sourcesContent":["/**\n * Per-participant control panel.\n *\n * Takes a `participant-id` and looks up the participant.\n * Shows mute audio, mute video, volume, pin, and remove actions based on\n * the `capabilities` list from the call state.\n *\n * Input precedence (most specific wins): `.call` > context.\n *\n * @prop {string} participantId - ID of the participant to control\n * @prop {Call} call - explicit Call object (overrides context)\n * @prop {boolean} showVolume - show volume slider\n * @prop {boolean} showPin - show pin/spotlight button\n *\n * @fires sw-participant-volume-change - Volume slider changed. Detail: `{ participantId: string, volume: number }`.\n * @fires sw-participant-pin-toggle - Pin/unpin clicked. Detail: `{ participantId: string, pinned: boolean }`.\n *\n * Themed via the SignalWire DTCG tokens (`--bg-page`, `--bg-surface`, `--bg-surface-raised`,\n * `--fg-default`, `--border-default`, `--radius-md`, `--shadow-md`, `--type-family-body`,\n * `--type-size-small`, `--interactive-button-destructive-bg`,\n * `--interactive-button-destructive-hover`, `--transition-fast`).\n */\n\nimport { LitElement, html, css, nothing } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { consume } from '@lit/context';\nimport { Subscription } from 'rxjs';\nimport type { CallParticipant } from '@signalwire/js';\nimport { callStateContext, type CallState } from '../context/call-state-context.js';\nimport type { Call } from '../types/index.js';\nimport { getLogger } from '@signalwire/js';\nimport './UI/icons/sw-ui-icon.js';\nimport { hostReset } from './UI/host-reset.js';\n\nconst logger = getLogger();\n\n@customElement('sw-participant-controls')\nexport class SwParticipantControls extends LitElement {\n static styles = [hostReset, css`\n :host {\n display: block;\n font-family: var(--type-family-body);\n font-size: var(--type-size-small);\n }\n\n .panel {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px;\n background: var(--bg-page);\n border: 1px solid var(--border-default);\n border-radius: var(--radius-md);\n min-width: 180px;\n box-shadow: var(--shadow-md);\n color: var(--fg-default);\n }\n\n .name {\n font-size: 0.8125rem;\n font-weight: 500;\n padding-bottom: 6px;\n border-bottom: 1px solid var(--border-default);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .actions {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n button {\n all: unset;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 10px;\n border-radius: var(--radius-md);\n font-size: 0.75rem;\n cursor: pointer;\n background: var(--bg-surface);\n color: var(--fg-default);\n transition: background var(--transition-fast);\n }\n\n button:hover {\n background: var(--bg-surface-raised);\n }\n\n button.active {\n background: var(--interactive-button-destructive-bg);\n color: #fff;\n }\n\n button.active:hover {\n background: var(--interactive-button-destructive-hover);\n }\n\n button.danger {\n color: var(--interactive-button-destructive-bg);\n }\n\n button.danger:hover {\n background: var(--interactive-button-destructive-bg);\n color: #fff;\n }\n\n .volume-control {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 4px 0;\n }\n\n .volume-label {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 0.7rem;\n color: color-mix(in srgb, var(--fg-default) 50%, transparent);\n }\n\n input[type='range'] {\n width: 100%;\n height: 4px;\n appearance: none;\n background: var(--border-default);\n border-radius: 2px;\n outline: none;\n cursor: pointer;\n }\n\n input[type='range']::-webkit-slider-thumb {\n appearance: none;\n width: 12px;\n height: 12px;\n background: #3b82f6;\n border-radius: 50%;\n cursor: pointer;\n }\n\n input[type='range']::-moz-range-thumb {\n width: 12px;\n height: 12px;\n background: #3b82f6;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n }\n\n .empty {\n font-size: 0.7rem;\n color: color-mix(in srgb, var(--fg-default) 50%, transparent);\n text-align: center;\n padding: 6px;\n }\n `];\n\n @property({ type: String, attribute: 'participant-id' })\n participantId = '';\n\n @property({ type: Boolean, reflect: true, attribute: 'show-volume' })\n showVolume = false;\n\n @property({ type: Boolean, reflect: true, attribute: 'show-pin' })\n showPin = false;\n\n @consume({ context: callStateContext, subscribe: true })\n @state()\n private _callState?: CallState;\n\n /** Explicit Call — when set, subscribes directly and bypasses context. */\n @property({ type: Object }) call?: Call;\n\n @state() private _directParticipants: CallParticipant[] = [];\n @state() private _directCapabilities: string[] = [];\n\n private _directSubscriptions: Subscription[] = [];\n\n @state() private _volume = 100;\n @state() private _isPinned = false;\n\n protected updated(changed: Map<string, unknown>): void {\n super.updated(changed);\n if (changed.has('call')) {\n this._directSubscriptions.forEach((s) => s.unsubscribe());\n this._directSubscriptions = [];\n this._directParticipants = [];\n this._directCapabilities = [];\n if (this.call) {\n this._directSubscriptions.push(\n this.call.participants$.subscribe((p) => (this._directParticipants = p)),\n this.call.capabilities$.subscribe((c) => (this._directCapabilities = c))\n );\n }\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this._directSubscriptions.forEach((s) => s.unsubscribe());\n this._directSubscriptions = [];\n }\n\n // ── Derived state ──────────────────────────────────────────────────\n\n private get _participant() {\n const participants = this.call ? this._directParticipants : this._callState?.participants ?? [];\n return participants.find((p) => p.id === this.participantId);\n }\n\n private get _capabilities(): string[] {\n return this.call ? this._directCapabilities : this._callState?.capabilities ?? [];\n }\n\n private get _canMuteAudio(): boolean {\n return this._capabilities.includes('memberMuteAudio');\n }\n\n private get _canMuteVideo(): boolean {\n return this._capabilities.includes('memberMuteVideo');\n }\n\n private get _canRemove(): boolean {\n return this._capabilities.includes('memberRemove');\n }\n\n // ── Actions ────────────────────────────────────────────────────────\n\n private async _toggleAudioMute() {\n const p = this._participant;\n if (!p) return;\n try {\n if (p.audioMuted) {\n await p.unmute();\n } else {\n await p.mute();\n }\n } catch (err) {\n logger.error('[ParticipantControls] Toggle audio failed:', err);\n }\n }\n\n private async _toggleVideoMute() {\n const p = this._participant;\n if (!p) return;\n try {\n if (p.videoMuted) {\n await p.unmuteVideo();\n } else {\n await p.muteVideo();\n }\n } catch (err) {\n logger.error('[ParticipantControls] Toggle video failed:', err);\n }\n }\n\n private async _remove() {\n const p = this._participant;\n if (!p?.remove) return;\n try {\n await p.remove();\n } catch (err) {\n logger.error('[ParticipantControls] Remove failed:', err);\n }\n }\n\n private _onVolumeChange(e: Event) {\n this._volume = parseInt((e.target as HTMLInputElement).value, 10);\n this.dispatchEvent(\n new CustomEvent('sw-participant-volume-change', {\n detail: { participantId: this.participantId, volume: this._volume },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n private _togglePin() {\n this._isPinned = !this._isPinned;\n this.dispatchEvent(\n new CustomEvent('sw-participant-pin-toggle', {\n detail: { participantId: this.participantId, pinned: this._isPinned },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n // ── Render ─────────────────────────────────────────────────────────\n\n render() {\n const p = this._participant;\n if (!p) return html`<div class=\"panel\"><div class=\"empty\">Participant not found</div></div>`;\n\n const hasActions = this._canMuteAudio || this._canMuteVideo || this._canRemove || this.showVolume || this.showPin;\n\n return html`\n <div class=\"panel\" part=\"panel\">\n <div class=\"name\" part=\"name\">${p.name ?? 'Participant'}</div>\n\n <div class=\"actions\">\n ${!hasActions ? html`<div class=\"empty\">No actions available</div>` : nothing}\n\n ${this._canMuteAudio\n ? html`\n <button class=${p.audioMuted ? 'active' : ''} @click=${this._toggleAudioMute}>\n <sw-ui-icon name=${p.audioMuted ? 'mic-off' : 'mic-on'} size=\"14\"></sw-ui-icon>\n ${p.audioMuted ? 'Unmute' : 'Mute'}\n </button>\n `\n : nothing}\n\n ${this._canMuteVideo\n ? html`\n <button class=${p.videoMuted ? 'active' : ''} @click=${this._toggleVideoMute}>\n <sw-ui-icon name=${p.videoMuted ? 'camera-off' : 'camera-on'} size=\"14\"></sw-ui-icon>\n ${p.videoMuted ? 'Enable video' : 'Disable video'}\n </button>\n `\n : nothing}\n\n ${this.showVolume\n ? html`\n <div class=\"volume-control\">\n <label class=\"volume-label\">\n <sw-ui-icon name=\"speaker-on\" size=\"14\"></sw-ui-icon>\n Volume: ${this._volume}%\n </label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"100\"\n .value=${String(this._volume)}\n @input=${this._onVolumeChange}\n />\n </div>\n `\n : nothing}\n\n ${this.showPin\n ? html`\n <button class=${this._isPinned ? 'active' : ''} @click=${this._togglePin}>\n <sw-ui-icon name=\"settings\" size=\"14\"></sw-ui-icon>\n ${this._isPinned ? 'Unpin' : 'Pin'}\n </button>\n `\n : nothing}\n\n ${this._canRemove\n ? html`\n <button class=\"danger\" @click=${this._remove}>\n <sw-ui-icon name=\"close\" size=\"14\"></sw-ui-icon>\n Remove\n </button>\n `\n : nothing}\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sw-participant-controls': SwParticipantControls;\n }\n}\n"],"names":["logger","getLogger","SwParticipantControls","LitElement","changed","s","p","c","_a","err","e","html","hasActions","nothing","hostReset","css","__decorateClass","property","consume","callStateContext","state","customElement"],"mappings":"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"}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { LitElement } from 'lit';
|
|
23
23
|
import type { Call } from '../types/index.js';
|
|
24
24
|
export declare class SwParticipants extends LitElement {
|
|
25
|
-
static styles: import("lit").CSSResult;
|
|
25
|
+
static styles: import("lit").CSSResult[];
|
|
26
26
|
private _callState?;
|
|
27
27
|
/** Explicit Call — when set, subscribes directly and bypasses context. */
|
|
28
28
|
call?: Call;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw-participants.d.ts","sourceRoot":"","sources":["../../src/components/sw-participants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,OAAO,KAAK,EAAE,IAAI,EAAe,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"sw-participants.d.ts","sourceRoot":"","sources":["../../src/components/sw-participants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,OAAO,KAAK,EAAE,IAAI,EAAe,MAAM,mBAAmB,CAAC;AAI3D,qBACa,cAAe,SAAQ,UAAU;IAC5C,MAAM,CAAC,MAAM,4BAiHV;IAIH,OAAO,CAAC,UAAU,CAAC,CAAY;IAE/B,0EAA0E;IAC9C,IAAI,CAAC,EAAE,IAAI,CAAC;IAE/B,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,mBAAmB,CAAyB;IACpD,OAAO,CAAC,WAAW,CAAoC;IAEhE,OAAO,CAAC,oBAAoB,CAAsB;IAGlD,OAAO,CAAC,WAAW,CAAuB;IAE1C,OAAO,CAAC,mBAAmB,CAKzB;IAEF,OAAO,CAAC,aAAa,CAInB;IAEF,iBAAiB;IAMjB,oBAAoB;IAOpB,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IActD,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,eAAe;YAKT,gBAAgB;YAuBhB,gBAAgB;YAuBhB,aAAa;IAqB3B,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,mBAAmB;IAsC3B,OAAO,CAAC,cAAc;IAuCtB,MAAM;CAaP;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,cAAc,CAAC;KACnC;CACF"}
|
|
@@ -2,12 +2,13 @@ import { LitElement as p, html as l, css as h } from "lit";
|
|
|
2
2
|
import { state as d, property as b, customElement as m } from "lit/decorators.js";
|
|
3
3
|
import { consume as g } from "@lit/context";
|
|
4
4
|
import { callStateContext as _ } from "../context/call-state-context.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { hostReset as v } from "./UI/host-reset.js";
|
|
6
|
+
var f = Object.defineProperty, y = Object.getOwnPropertyDescriptor, a = (e, t, i, s) => {
|
|
7
|
+
for (var r = s > 1 ? void 0 : s ? y(t, i) : t, n = e.length - 1, c; n >= 0; n--)
|
|
8
|
+
(c = e[n]) && (r = (s ? c(t, i, r) : c(r)) || r);
|
|
9
|
+
return s && r && f(t, i, r), r;
|
|
9
10
|
};
|
|
10
|
-
let
|
|
11
|
+
let o = class extends p {
|
|
11
12
|
constructor() {
|
|
12
13
|
super(...arguments), this._directLayoutLayers = [], this._directParticipants = [], this._directSelf = null, this._directSubscriptions = [], this._openMenuId = null, this._handleOutsideClick = (e) => {
|
|
13
14
|
const t = e.target;
|
|
@@ -155,7 +156,7 @@ let n = class extends p {
|
|
|
155
156
|
<button
|
|
156
157
|
class="menu-trigger"
|
|
157
158
|
part="menu-trigger"
|
|
158
|
-
@click=${(
|
|
159
|
+
@click=${(n) => this._toggleMenu(i, n)}
|
|
159
160
|
aria-label="Participant menu"
|
|
160
161
|
aria-expanded="${this._openMenuId === i}"
|
|
161
162
|
aria-haspopup="menu"
|
|
@@ -169,15 +170,15 @@ let n = class extends p {
|
|
|
169
170
|
`;
|
|
170
171
|
}
|
|
171
172
|
render() {
|
|
172
|
-
var s, r,
|
|
173
|
-
const e = this.call ? this._directLayoutLayers : ((s = this._callState) == null ? void 0 : s.layoutLayers) ?? [], t = this.call ? (r = this._directSelf) == null ? void 0 : r.id : (c = (
|
|
173
|
+
var s, r, n, c;
|
|
174
|
+
const e = this.call ? this._directLayoutLayers : ((s = this._callState) == null ? void 0 : s.layoutLayers) ?? [], t = this.call ? (r = this._directSelf) == null ? void 0 : r.id : (c = (n = this._callState) == null ? void 0 : n.self) == null ? void 0 : c.id, i = e.filter((u) => u.member_id).map((u) => this._renderOverlay(u, u.member_id === t));
|
|
174
175
|
return l`
|
|
175
176
|
${i}
|
|
176
177
|
<slot></slot>
|
|
177
178
|
`;
|
|
178
179
|
}
|
|
179
180
|
};
|
|
180
|
-
|
|
181
|
+
o.styles = [v, h`
|
|
181
182
|
:host {
|
|
182
183
|
display: contents;
|
|
183
184
|
}
|
|
@@ -290,30 +291,30 @@ n.styles = h`
|
|
|
290
291
|
.menu-item.danger:hover {
|
|
291
292
|
background: rgba(239, 68, 68, 0.2);
|
|
292
293
|
}
|
|
293
|
-
|
|
294
|
+
`];
|
|
294
295
|
a([
|
|
295
296
|
g({ context: _, subscribe: !0 }),
|
|
296
297
|
d()
|
|
297
|
-
],
|
|
298
|
+
], o.prototype, "_callState", 2);
|
|
298
299
|
a([
|
|
299
300
|
b({ type: Object })
|
|
300
|
-
],
|
|
301
|
+
], o.prototype, "call", 2);
|
|
301
302
|
a([
|
|
302
303
|
d()
|
|
303
|
-
],
|
|
304
|
+
], o.prototype, "_directLayoutLayers", 2);
|
|
304
305
|
a([
|
|
305
306
|
d()
|
|
306
|
-
],
|
|
307
|
+
], o.prototype, "_directParticipants", 2);
|
|
307
308
|
a([
|
|
308
309
|
d()
|
|
309
|
-
],
|
|
310
|
+
], o.prototype, "_directSelf", 2);
|
|
310
311
|
a([
|
|
311
312
|
d()
|
|
312
|
-
],
|
|
313
|
-
|
|
313
|
+
], o.prototype, "_openMenuId", 2);
|
|
314
|
+
o = a([
|
|
314
315
|
m("sw-participants")
|
|
315
|
-
],
|
|
316
|
+
], o);
|
|
316
317
|
export {
|
|
317
|
-
|
|
318
|
+
o as SwParticipants
|
|
318
319
|
};
|
|
319
320
|
//# sourceMappingURL=sw-participants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw-participants.js","sources":["../../src/components/sw-participants.ts"],"sourcesContent":["/**\n * SwParticipants Component\n *\n * Renders member overlays based on layoutLayers.\n * Excludes self member and provides slot for <self-media> child component.\n *\n * Input precedence (most specific wins): `.call` > context.\n *\n * @example\n * ```html\n * <sw-participants></sw-participants>\n * <sw-participants .call=${call}></sw-participants>\n * ```\n *\n * @slot - Default slot for overlay content.\n * @slot controls-{memberId} - Per-participant menu content (e.g. `sw-participant-controls`).\n *\n * @fires sw-participant-mute-audio - User muted/unmuted a participant's audio. Detail: `{ participant, memberId }`.\n * @fires sw-participant-mute-video - User muted/unmuted a participant's video. Detail: `{ participant, memberId }`.\n * @fires sw-participant-remove - User removed a participant. Detail: `{ participant, memberId }`.\n */\n\nimport { LitElement, html, css } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { consume } from '@lit/context';\nimport { Subscription } from 'rxjs';\nimport type { CallParticipant, CallSelfParticipant } from '@signalwire/js';\nimport type { Call, LayoutLayer } from '../types/index.js';\nimport { callStateContext, type CallState } from '../context/call-state-context.js';\n\n@customElement('sw-participants')\nexport class SwParticipants extends LitElement {\n static styles = css`\n :host {\n display: contents;\n }\n\n .member-overlay {\n box-sizing: border-box;\n box-shadow: inset 0 0 0 8px rgba(255, 0, 0, 0.8);\n background-color: rgba(255, 0, 0, 0.1);\n }\n\n .member-overlay.is-self {\n box-shadow: inset 0 0 0 8px rgba(0, 0, 255, 0.8);\n background-color: rgba(0, 0, 255, 0.1);\n }\n\n .menu-trigger {\n position: absolute;\n top: 12px;\n left: 12px;\n width: 36px;\n height: 36px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.6);\n border: 2px solid rgba(255, 255, 255, 0.3);\n color: white;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s ease,\n transform 0.2s ease,\n border-color 0.2s ease;\n z-index: 20;\n }\n\n .menu-trigger:hover {\n background: rgba(0, 0, 0, 0.8);\n border-color: rgba(255, 255, 255, 0.6);\n transform: scale(1.1);\n }\n\n .menu-trigger:focus {\n outline: none;\n border-color: #044cf6;\n box-shadow: 0 0 0 3px rgba(4, 78, 246, 0.4);\n }\n\n .menu-trigger svg {\n width: 20px;\n height: 20px;\n }\n\n .menu-dropdown {\n position: absolute;\n top: 12px;\n left: 56px;\n background: rgba(31, 41, 55, 0.95);\n backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 160px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n z-index: 30;\n opacity: 0;\n visibility: hidden;\n transform: translateX(-10px);\n transition:\n opacity 0.2s ease,\n transform 0.2s ease,\n visibility 0.2s ease;\n }\n\n .menu-dropdown.open {\n opacity: 1;\n visibility: visible;\n transform: translateX(0);\n }\n\n .menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n color: white;\n font-size: 14px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n cursor: pointer;\n transition: background 0.15s ease;\n border: none;\n background: none;\n width: 100%;\n text-align: left;\n }\n\n .menu-item:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n\n .menu-item svg {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n }\n\n .menu-item.danger {\n color: #ef4444;\n }\n\n .menu-item.danger:hover {\n background: rgba(239, 68, 68, 0.2);\n }\n `;\n\n @consume({ context: callStateContext, subscribe: true })\n @state()\n private _callState?: CallState;\n\n /** Explicit Call — when set, subscribes directly and bypasses context. */\n @property({ type: Object }) call?: Call;\n\n @state() private _directLayoutLayers: LayoutLayer[] = [];\n @state() private _directParticipants: CallParticipant[] = [];\n @state() private _directSelf: CallSelfParticipant | null = null;\n\n private _directSubscriptions: Subscription[] = [];\n\n @state()\n private _openMenuId: string | null = null;\n\n private _handleOutsideClick = (e: Event): void => {\n const target = e.target as Element;\n if (!target.closest('.menu-trigger') && !target.closest('.menu-dropdown')) {\n this._openMenuId = null;\n }\n };\n\n private _handleEscape = (e: KeyboardEvent): void => {\n if (e.key === 'Escape' && this._openMenuId !== null) {\n this._openMenuId = null;\n }\n };\n\n connectedCallback() {\n super.connectedCallback();\n document.addEventListener('click', this._handleOutsideClick);\n document.addEventListener('keydown', this._handleEscape);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n document.removeEventListener('click', this._handleOutsideClick);\n document.removeEventListener('keydown', this._handleEscape);\n this._teardownDirect();\n }\n\n protected updated(changed: Map<string, unknown>): void {\n super.updated(changed);\n if (changed.has('call')) {\n this._teardownDirect();\n if (this.call) {\n this._directSubscriptions.push(\n this.call.layoutLayers$.subscribe((l) => (this._directLayoutLayers = l)),\n this.call.participants$.subscribe((p) => (this._directParticipants = p)),\n this.call.self$.subscribe((s) => (this._directSelf = s))\n );\n }\n }\n }\n\n private _teardownDirect(): void {\n this._directSubscriptions.forEach((s) => s.unsubscribe());\n this._directSubscriptions = [];\n this._directLayoutLayers = [];\n this._directParticipants = [];\n this._directSelf = null;\n }\n\n // ── Helpers ────────────────────────────────────────────────────────\n\n private _toggleMenu(memberId: string, e: Event): void {\n e.stopPropagation();\n this._openMenuId = this._openMenuId === memberId ? null : memberId;\n }\n\n private _getParticipant(memberId: string) {\n const participants = this.call ? this._directParticipants : this._callState?.participants ?? [];\n return participants.find((p) => p.id === memberId);\n }\n\n private async _handleMuteAudio(memberId: string, e: Event): Promise<void> {\n e.stopPropagation();\n const participant = this._getParticipant(memberId);\n if (!participant) return;\n try {\n if (participant.audioMuted) {\n await participant.unmute();\n } else {\n await participant.mute();\n }\n this.dispatchEvent(\n new CustomEvent('sw-participant-mute-audio', {\n detail: { participant, memberId },\n bubbles: true,\n composed: true,\n })\n );\n } catch {\n // Silently handle error\n }\n this._openMenuId = null;\n }\n\n private async _handleMuteVideo(memberId: string, e: Event): Promise<void> {\n e.stopPropagation();\n const participant = this._getParticipant(memberId);\n if (!participant) return;\n try {\n if (participant.videoMuted) {\n await participant.unmuteVideo();\n } else {\n await participant.muteVideo();\n }\n this.dispatchEvent(\n new CustomEvent('sw-participant-mute-video', {\n detail: { participant, memberId },\n bubbles: true,\n composed: true,\n })\n );\n } catch {\n // Silently handle error\n }\n this._openMenuId = null;\n }\n\n private async _handleRemove(memberId: string, e: Event): Promise<void> {\n e.stopPropagation();\n const participant = this._getParticipant(memberId);\n if (!participant?.remove) return;\n try {\n await participant.remove();\n this.dispatchEvent(\n new CustomEvent('sw-participant-remove', {\n detail: { participant, memberId },\n bubbles: true,\n composed: true,\n })\n );\n } catch {\n // Silently handle error\n }\n this._openMenuId = null;\n }\n\n // ── Render ─────────────────────────────────────────────────────────\n\n private _renderMenuIcon() {\n return html`\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"6\" r=\"2\" />\n <circle cx=\"12\" cy=\"12\" r=\"2\" />\n <circle cx=\"12\" cy=\"18\" r=\"2\" />\n </svg>\n `;\n }\n\n private _renderMenuDropdown(memberId: string) {\n const participant = this._getParticipant(memberId);\n const isOpen = this._openMenuId === memberId;\n\n return html`\n <div\n class=\"menu-dropdown ${isOpen ? 'open' : ''}\"\n role=\"menu\"\n aria-label=\"Participant actions\"\n >\n <button\n class=\"menu-item\"\n role=\"menuitem\"\n @click=${(e: Event) => this._handleMuteAudio(memberId, e)}\n aria-label=\"${participant?.audioMuted ? 'Unmute audio' : 'Mute audio'}\"\n >\n ${participant?.audioMuted ? 'Unmute' : 'Mute'}\n </button>\n <button\n class=\"menu-item\"\n role=\"menuitem\"\n @click=${(e: Event) => this._handleMuteVideo(memberId, e)}\n aria-label=\"${participant?.videoMuted ? 'Enable video' : 'Disable video'}\"\n >\n ${participant?.videoMuted ? 'Enable video' : 'Disable video'}\n </button>\n <button\n class=\"menu-item danger\"\n role=\"menuitem\"\n @click=${(e: Event) => this._handleRemove(memberId, e)}\n aria-label=\"Remove participant\"\n >\n Remove\n </button>\n </div>\n `;\n }\n\n private _renderOverlay(layer: LayoutLayer, isSelf: boolean) {\n const memberId = layer.member_id!;\n const classes = `member-overlay${isSelf ? ' is-self' : ''}`;\n\n const style = `\n position: absolute;\n top: ${layer.y}%;\n left: ${layer.x}%;\n width: ${layer.width}%;\n height: ${layer.height}%;\n opacity: ${layer.visible ? 1 : 0};\n overflow: visible;\n transition: top 0.3s ease, left 0.3s ease, width 0.3s ease, height 0.3s ease, opacity 0.3s ease;\n pointer-events: auto;\n z-index: 10;\n `;\n\n return html`\n <div class=\"${classes}\" part=\"overlay\" style=\"${style}\">\n ${!isSelf\n ? html`\n <button\n class=\"menu-trigger\"\n part=\"menu-trigger\"\n @click=${(e: Event) => this._toggleMenu(memberId, e)}\n aria-label=\"Participant menu\"\n aria-expanded=\"${this._openMenuId === memberId}\"\n aria-haspopup=\"menu\"\n >\n ${this._renderMenuIcon()}\n </button>\n ${this._renderMenuDropdown(memberId)}\n `\n : null}\n <slot name=\"controls-${memberId}\"></slot>\n </div>\n `;\n }\n\n render() {\n const layers = this.call ? this._directLayoutLayers : this._callState?.layoutLayers ?? [];\n const selfId = this.call ? this._directSelf?.id : this._callState?.self?.id;\n\n const overlays = layers\n .filter((layer) => layer.member_id)\n .map((layer) => this._renderOverlay(layer, layer.member_id === selfId));\n\n return html`\n ${overlays}\n <slot></slot>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sw-participants': SwParticipants;\n }\n}\n"],"names":["SwParticipants","LitElement","target","changed","l","p","s","memberId","e","_a","participant","html","isOpen","layer","isSelf","classes","style","layers","selfId","_b","_d","_c","overlays","css","__decorateClass","consume","callStateContext","state","property","customElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sw-participants.js","sources":["../../src/components/sw-participants.ts"],"sourcesContent":["/**\n * SwParticipants Component\n *\n * Renders member overlays based on layoutLayers.\n * Excludes self member and provides slot for <self-media> child component.\n *\n * Input precedence (most specific wins): `.call` > context.\n *\n * @example\n * ```html\n * <sw-participants></sw-participants>\n * <sw-participants .call=${call}></sw-participants>\n * ```\n *\n * @slot - Default slot for overlay content.\n * @slot controls-{memberId} - Per-participant menu content (e.g. `sw-participant-controls`).\n *\n * @fires sw-participant-mute-audio - User muted/unmuted a participant's audio. Detail: `{ participant, memberId }`.\n * @fires sw-participant-mute-video - User muted/unmuted a participant's video. Detail: `{ participant, memberId }`.\n * @fires sw-participant-remove - User removed a participant. Detail: `{ participant, memberId }`.\n */\n\nimport { LitElement, html, css } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { consume } from '@lit/context';\nimport { Subscription } from 'rxjs';\nimport type { CallParticipant, CallSelfParticipant } from '@signalwire/js';\nimport type { Call, LayoutLayer } from '../types/index.js';\nimport { callStateContext, type CallState } from '../context/call-state-context.js';\nimport { hostReset } from './UI/host-reset.js';\n\n@customElement('sw-participants')\nexport class SwParticipants extends LitElement {\n static styles = [hostReset, css`\n :host {\n display: contents;\n }\n\n .member-overlay {\n box-sizing: border-box;\n box-shadow: inset 0 0 0 8px rgba(255, 0, 0, 0.8);\n background-color: rgba(255, 0, 0, 0.1);\n }\n\n .member-overlay.is-self {\n box-shadow: inset 0 0 0 8px rgba(0, 0, 255, 0.8);\n background-color: rgba(0, 0, 255, 0.1);\n }\n\n .menu-trigger {\n position: absolute;\n top: 12px;\n left: 12px;\n width: 36px;\n height: 36px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.6);\n border: 2px solid rgba(255, 255, 255, 0.3);\n color: white;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s ease,\n transform 0.2s ease,\n border-color 0.2s ease;\n z-index: 20;\n }\n\n .menu-trigger:hover {\n background: rgba(0, 0, 0, 0.8);\n border-color: rgba(255, 255, 255, 0.6);\n transform: scale(1.1);\n }\n\n .menu-trigger:focus {\n outline: none;\n border-color: #044cf6;\n box-shadow: 0 0 0 3px rgba(4, 78, 246, 0.4);\n }\n\n .menu-trigger svg {\n width: 20px;\n height: 20px;\n }\n\n .menu-dropdown {\n position: absolute;\n top: 12px;\n left: 56px;\n background: rgba(31, 41, 55, 0.95);\n backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 160px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n z-index: 30;\n opacity: 0;\n visibility: hidden;\n transform: translateX(-10px);\n transition:\n opacity 0.2s ease,\n transform 0.2s ease,\n visibility 0.2s ease;\n }\n\n .menu-dropdown.open {\n opacity: 1;\n visibility: visible;\n transform: translateX(0);\n }\n\n .menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n color: white;\n font-size: 14px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n cursor: pointer;\n transition: background 0.15s ease;\n border: none;\n background: none;\n width: 100%;\n text-align: left;\n }\n\n .menu-item:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n\n .menu-item svg {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n }\n\n .menu-item.danger {\n color: #ef4444;\n }\n\n .menu-item.danger:hover {\n background: rgba(239, 68, 68, 0.2);\n }\n `];\n\n @consume({ context: callStateContext, subscribe: true })\n @state()\n private _callState?: CallState;\n\n /** Explicit Call — when set, subscribes directly and bypasses context. */\n @property({ type: Object }) call?: Call;\n\n @state() private _directLayoutLayers: LayoutLayer[] = [];\n @state() private _directParticipants: CallParticipant[] = [];\n @state() private _directSelf: CallSelfParticipant | null = null;\n\n private _directSubscriptions: Subscription[] = [];\n\n @state()\n private _openMenuId: string | null = null;\n\n private _handleOutsideClick = (e: Event): void => {\n const target = e.target as Element;\n if (!target.closest('.menu-trigger') && !target.closest('.menu-dropdown')) {\n this._openMenuId = null;\n }\n };\n\n private _handleEscape = (e: KeyboardEvent): void => {\n if (e.key === 'Escape' && this._openMenuId !== null) {\n this._openMenuId = null;\n }\n };\n\n connectedCallback() {\n super.connectedCallback();\n document.addEventListener('click', this._handleOutsideClick);\n document.addEventListener('keydown', this._handleEscape);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n document.removeEventListener('click', this._handleOutsideClick);\n document.removeEventListener('keydown', this._handleEscape);\n this._teardownDirect();\n }\n\n protected updated(changed: Map<string, unknown>): void {\n super.updated(changed);\n if (changed.has('call')) {\n this._teardownDirect();\n if (this.call) {\n this._directSubscriptions.push(\n this.call.layoutLayers$.subscribe((l) => (this._directLayoutLayers = l)),\n this.call.participants$.subscribe((p) => (this._directParticipants = p)),\n this.call.self$.subscribe((s) => (this._directSelf = s))\n );\n }\n }\n }\n\n private _teardownDirect(): void {\n this._directSubscriptions.forEach((s) => s.unsubscribe());\n this._directSubscriptions = [];\n this._directLayoutLayers = [];\n this._directParticipants = [];\n this._directSelf = null;\n }\n\n // ── Helpers ────────────────────────────────────────────────────────\n\n private _toggleMenu(memberId: string, e: Event): void {\n e.stopPropagation();\n this._openMenuId = this._openMenuId === memberId ? null : memberId;\n }\n\n private _getParticipant(memberId: string) {\n const participants = this.call ? this._directParticipants : this._callState?.participants ?? [];\n return participants.find((p) => p.id === memberId);\n }\n\n private async _handleMuteAudio(memberId: string, e: Event): Promise<void> {\n e.stopPropagation();\n const participant = this._getParticipant(memberId);\n if (!participant) return;\n try {\n if (participant.audioMuted) {\n await participant.unmute();\n } else {\n await participant.mute();\n }\n this.dispatchEvent(\n new CustomEvent('sw-participant-mute-audio', {\n detail: { participant, memberId },\n bubbles: true,\n composed: true,\n })\n );\n } catch {\n // Silently handle error\n }\n this._openMenuId = null;\n }\n\n private async _handleMuteVideo(memberId: string, e: Event): Promise<void> {\n e.stopPropagation();\n const participant = this._getParticipant(memberId);\n if (!participant) return;\n try {\n if (participant.videoMuted) {\n await participant.unmuteVideo();\n } else {\n await participant.muteVideo();\n }\n this.dispatchEvent(\n new CustomEvent('sw-participant-mute-video', {\n detail: { participant, memberId },\n bubbles: true,\n composed: true,\n })\n );\n } catch {\n // Silently handle error\n }\n this._openMenuId = null;\n }\n\n private async _handleRemove(memberId: string, e: Event): Promise<void> {\n e.stopPropagation();\n const participant = this._getParticipant(memberId);\n if (!participant?.remove) return;\n try {\n await participant.remove();\n this.dispatchEvent(\n new CustomEvent('sw-participant-remove', {\n detail: { participant, memberId },\n bubbles: true,\n composed: true,\n })\n );\n } catch {\n // Silently handle error\n }\n this._openMenuId = null;\n }\n\n // ── Render ─────────────────────────────────────────────────────────\n\n private _renderMenuIcon() {\n return html`\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"6\" r=\"2\" />\n <circle cx=\"12\" cy=\"12\" r=\"2\" />\n <circle cx=\"12\" cy=\"18\" r=\"2\" />\n </svg>\n `;\n }\n\n private _renderMenuDropdown(memberId: string) {\n const participant = this._getParticipant(memberId);\n const isOpen = this._openMenuId === memberId;\n\n return html`\n <div\n class=\"menu-dropdown ${isOpen ? 'open' : ''}\"\n role=\"menu\"\n aria-label=\"Participant actions\"\n >\n <button\n class=\"menu-item\"\n role=\"menuitem\"\n @click=${(e: Event) => this._handleMuteAudio(memberId, e)}\n aria-label=\"${participant?.audioMuted ? 'Unmute audio' : 'Mute audio'}\"\n >\n ${participant?.audioMuted ? 'Unmute' : 'Mute'}\n </button>\n <button\n class=\"menu-item\"\n role=\"menuitem\"\n @click=${(e: Event) => this._handleMuteVideo(memberId, e)}\n aria-label=\"${participant?.videoMuted ? 'Enable video' : 'Disable video'}\"\n >\n ${participant?.videoMuted ? 'Enable video' : 'Disable video'}\n </button>\n <button\n class=\"menu-item danger\"\n role=\"menuitem\"\n @click=${(e: Event) => this._handleRemove(memberId, e)}\n aria-label=\"Remove participant\"\n >\n Remove\n </button>\n </div>\n `;\n }\n\n private _renderOverlay(layer: LayoutLayer, isSelf: boolean) {\n const memberId = layer.member_id!;\n const classes = `member-overlay${isSelf ? ' is-self' : ''}`;\n\n const style = `\n position: absolute;\n top: ${layer.y}%;\n left: ${layer.x}%;\n width: ${layer.width}%;\n height: ${layer.height}%;\n opacity: ${layer.visible ? 1 : 0};\n overflow: visible;\n transition: top 0.3s ease, left 0.3s ease, width 0.3s ease, height 0.3s ease, opacity 0.3s ease;\n pointer-events: auto;\n z-index: 10;\n `;\n\n return html`\n <div class=\"${classes}\" part=\"overlay\" style=\"${style}\">\n ${!isSelf\n ? html`\n <button\n class=\"menu-trigger\"\n part=\"menu-trigger\"\n @click=${(e: Event) => this._toggleMenu(memberId, e)}\n aria-label=\"Participant menu\"\n aria-expanded=\"${this._openMenuId === memberId}\"\n aria-haspopup=\"menu\"\n >\n ${this._renderMenuIcon()}\n </button>\n ${this._renderMenuDropdown(memberId)}\n `\n : null}\n <slot name=\"controls-${memberId}\"></slot>\n </div>\n `;\n }\n\n render() {\n const layers = this.call ? this._directLayoutLayers : this._callState?.layoutLayers ?? [];\n const selfId = this.call ? this._directSelf?.id : this._callState?.self?.id;\n\n const overlays = layers\n .filter((layer) => layer.member_id)\n .map((layer) => this._renderOverlay(layer, layer.member_id === selfId));\n\n return html`\n ${overlays}\n <slot></slot>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sw-participants': SwParticipants;\n }\n}\n"],"names":["SwParticipants","LitElement","target","changed","l","p","s","memberId","e","_a","participant","html","isOpen","layer","isSelf","classes","style","layers","selfId","_b","_d","_c","overlays","hostReset","css","__decorateClass","consume","callStateContext","state","property","customElement"],"mappings":"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"}
|