@pecb-ui/components 1.1.10 → 1.1.11

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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, input, output, HostBinding, Component, ChangeDetectionStrategy, model, computed, signal, HostListener, ContentChild, effect, forwardRef, ViewChild, ViewContainerRef, viewChild, ViewEncapsulation, Directive, untracked, ElementRef, ApplicationRef, EnvironmentInjector, createComponent, contentChildren, PLATFORM_ID, Inject, ViewChildren, Renderer2, NgZone } from '@angular/core';
2
+ import { Injectable, inject, input, output, HostBinding, Component, ChangeDetectionStrategy, model, computed, signal, HostListener, ContentChild, effect, forwardRef, ViewChild, ViewContainerRef, viewChild, ViewEncapsulation, Directive, untracked, ElementRef, ApplicationRef, EnvironmentInjector, createComponent, contentChildren, PLATFORM_ID, NgZone, DestroyRef, booleanAttribute, numberAttribute, afterNextRender, Injector, Inject, ViewChildren, Renderer2 } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
- import { CommonModule, NgOptimizedImage, isPlatformBrowser } from '@angular/common';
4
+ import { CommonModule, NgOptimizedImage, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
5
5
  import * as i1$1 from '@angular/platform-browser';
6
6
  import { DomSanitizer } from '@angular/platform-browser';
7
7
  import * as i2 from '@angular/cdk/overlay';
@@ -4525,11 +4525,11 @@ class TableComponent {
4525
4525
  this.selectionChange.emit({ selectedRows, selectedIndices: this.selectedIndices() });
4526
4526
  }
4527
4527
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: TableComponent, deps: [{ token: i1$1.DomSanitizer }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
4528
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: TableComponent, isStandalone: true, selector: "pecb-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, hoverable: { classPropertyName: "hoverable", publicName: "hoverable", isSignal: true, isRequired: false, transformFunction: null }, showPagination: { classPropertyName: "showPagination", publicName: "showPagination", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, showPageSizeSelector: { classPropertyName: "showPageSizeSelector", publicName: "showPageSizeSelector", isSignal: true, isRequired: false, transformFunction: null }, paginationVariant: { classPropertyName: "paginationVariant", publicName: "paginationVariant", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, selectedIndices: { classPropertyName: "selectedIndices", publicName: "selectedIndices", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, wrapText: { classPropertyName: "wrapText", publicName: "wrapText", isSignal: true, isRequired: false, transformFunction: null }, sortColumn: { classPropertyName: "sortColumn", publicName: "sortColumn", isSignal: true, isRequired: false, transformFunction: null }, sortDirection: { classPropertyName: "sortDirection", publicName: "sortDirection", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, selectAllAriaLabel: { classPropertyName: "selectAllAriaLabel", publicName: "selectAllAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, selectRowAriaLabel: { classPropertyName: "selectRowAriaLabel", publicName: "selectRowAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", pageSize: "pageSizeChange", selectedIndices: "selectedIndicesChange", sortColumn: "sortColumnChange", sortDirection: "sortDirectionChange", pageChange: "pageChange", rowClick: "rowClick", rowAction: "rowAction", sortChange: "sortChange", selectionChange: "selectionChange" }, queries: [{ propertyName: "emptyTemplate", first: true, predicate: ["emptyTemplate"], descendants: true }, { propertyName: "loadingTemplate", first: true, predicate: ["loadingTemplate"], descendants: true }], ngImport: i0, template: "<div class=\"pecb-table-wrapper\">\n <div [class]=\"containerClasses\" [style.maxHeight]=\"maxHeight()\">\n <!-- Loading State -->\n <div *ngIf=\"loading()\" class=\"pecb-table__loading\">\n <ng-container *ngIf=\"loadingTemplate; else defaultLoading\">\n <ng-container *ngTemplateOutlet=\"loadingTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultLoading>\n <div class=\"pecb-table__loading-spinner\">\n <svg class=\"pecb-table__spinner-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-dasharray=\"31.4 31.4\" />\n </svg>\n <span>{{ loadingText() }}</span>\n </div>\n </ng-template>\n </div>\n\n <!-- Table -->\n <table\n *ngIf=\"!loading()\"\n [class]=\"tableClasses\"\n [style.min-width]=\"minWidth()\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"table\">\n\n <!-- Table Header -->\n <thead *ngIf=\"showHeader()\" class=\"pecb-table__head\" [class.pecb-table__head--sticky]=\"stickyHeader()\">\n <tr class=\"pecb-table__row pecb-table__row--header\" role=\"row\">\n <!-- Selection Checkbox Column -->\n <th\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--header pecb-table__cell--checkbox\"\n role=\"columnheader\"\n scope=\"col\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isSomeSelected()\"\n (change)=\"toggleAllSelection()\"\n [attr.aria-label]=\"selectAllAriaLabel()\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </th>\n\n <!-- Data Columns -->\n <th\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell pecb-table__cell--header\"\n [class.pecb-table__cell--sortable]=\"column.sortable\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [style.width]=\"column.width\"\n [attr.aria-sort]=\"column.sortable ? (sortColumn() === column.key ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : 'none') : null\"\n role=\"columnheader\"\n scope=\"col\"\n (click)=\"onHeaderClick(column)\"\n (keydown.enter)=\"onHeaderClick(column)\"\n (keydown.space)=\"onHeaderClick(column); $event.preventDefault()\"\n [attr.tabindex]=\"column.sortable ? 0 : null\">\n <div class=\"pecb-table__header-content\">\n <ng-container *ngIf=\"column.headerTemplate; else defaultHeader\">\n <ng-container *ngTemplateOutlet=\"column.headerTemplate; context: { $implicit: column }\"></ng-container>\n </ng-container>\n <ng-template #defaultHeader>\n <span class=\"pecb-table__header-text\">{{ column.label }}</span>\n </ng-template>\n\n <!-- Sort Icon -->\n <span *ngIf=\"column.sortable\" class=\"pecb-table__sort-icon\" [attr.data-sort]=\"getSortState(column)\">\n <svg *ngIf=\"getSortState(column) === 'none'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'asc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'desc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\"/>\n </svg>\n </span>\n </div>\n </th>\n </tr>\n </thead>\n\n <!-- Table Body -->\n <tbody class=\"pecb-table__body\">\n <!-- Empty State -->\n <tr *ngIf=\"data().length === 0\" class=\"pecb-table__row pecb-table__row--empty\">\n <td\n [attr.colspan]=\"selectable() ? columns().length + 1 : columns().length\"\n class=\"pecb-table__cell pecb-table__cell--empty\">\n <ng-container *ngIf=\"emptyTemplate; else defaultEmpty\">\n <ng-container *ngTemplateOutlet=\"emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"pecb-table__empty-content\">\n <span class=\"pecb-table__empty-message\">{{ emptyMessage() }}</span>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data Rows -->\n <tr\n *ngFor=\"let row of data(); let i = index; trackBy: trackByIndex\"\n class=\"pecb-table__row pecb-table__row--data\"\n [class.pecb-table__row--selected]=\"isRowSelected(i)\"\n role=\"row\"\n (click)=\"onRowClick(row, i)\"\n [attr.tabindex]=\"0\"\n (keydown.enter)=\"onRowClick(row, i)\">\n\n <!-- Selection Checkbox -->\n <td\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--checkbox\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isRowSelected(i)\"\n (change)=\"toggleRowSelection(i)\"\n [attr.aria-label]=\"selectRowAriaLabel() + ' ' + (i + 1)\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </td>\n\n <!-- Data Cells -->\n <td\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [class.pecb-table__cell--wrap]=\"isCellWrapped(column)\"\n [class.pecb-table__cell--truncate]=\"!isCellWrapped(column)\"\n [style.max-width]=\"column.maxWidth || column.width\"\n [attr.title]=\"getCellTitle(row, column)\"\n role=\"cell\">\n\n <!-- Custom Template -->\n <ng-container *ngIf=\"column.cellTemplate\">\n <ng-container *ngTemplateOutlet=\"column.cellTemplate; context: { $implicit: getCellValue(row, column.key), row: row, column: column, index: i }\"></ng-container>\n </ng-container>\n\n <!-- Dynamic Component - renders any component passed via dynamicComponent -->\n <ng-container *ngIf=\"!column.cellTemplate && hasDynamicComponent(column)\">\n <ng-container\n *ngComponentOutlet=\"column.dynamicComponent!; inputs: getDynamicComponentInputs(row, column, i)\">\n </ng-container>\n </ng-container>\n\n <!-- Status Component - renders pecb-status -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'status'\">\n <pecb-status\n [type]=\"getStatusType(getCellValue(row, column.key), column)\"\n [variant]=\"getStatusVariant(column)\"\n [size]=\"getStatusSize(column)\">\n {{ getCellValue(row, column.key) }}\n </pecb-status>\n </ng-container>\n\n <!-- User Component - renders avatar + name + email -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'user'\">\n <div class=\"pecb-table__user-cell\">\n <img\n *ngIf=\"getUserData(row, column).avatar\"\n [src]=\"getUserData(row, column).avatar\"\n alt=\"\"\n class=\"pecb-table__user-avatar\">\n <div class=\"pecb-table__user-info\">\n <div class=\"pecb-table__user-name\">{{ getUserData(row, column).name }}</div>\n <div *ngIf=\"getUserData(row, column).secondary\" class=\"pecb-table__user-secondary\">\n {{ getUserData(row, column).secondary }}\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Action Component - renders action buttons with icons -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'action'\">\n <div class=\"pecb-table__action-buttons\">\n <ng-container *ngFor=\"let action of getActions(column)\">\n <!-- Divider before this action (consumer-controlled grouping) -->\n <span *ngIf=\"action.dividerBefore\" class=\"pecb-table__action-divider\"></span>\n <button\n class=\"pecb-table__action-btn\"\n [class.pecb-table__action-btn--danger]=\"action.variant === 'danger'\"\n [title]=\"action.tooltip || action.id\"\n [disabled]=\"action.disabled\"\n (click)=\"onActionClick(action, row, i, $event)\">\n <!-- View Icon -->\n <svg *ngIf=\"action.icon === 'view'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"></path>\n <circle cx=\"12\" cy=\"12\" r=\"3\"></circle>\n </svg>\n <!-- Edit Icon -->\n <svg *ngIf=\"action.icon === 'edit'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"></path>\n <path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"></path>\n </svg>\n <!-- Delete Icon -->\n <svg *ngIf=\"action.icon === 'delete'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"></polyline>\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path>\n </svg>\n <!-- Download Icon -->\n <svg *ngIf=\"action.icon === 'download'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path>\n <polyline points=\"7 10 12 15 17 10\"></polyline>\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line>\n </svg>\n <!-- Copy Icon -->\n <svg *ngIf=\"action.icon === 'copy'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n </svg>\n <!-- Custom Icon (SVG string) -->\n <span *ngIf=\"action.icon === 'custom' && action.customIcon\" class=\"pecb-table__custom-icon\" [innerHTML]=\"getSafeIcon(action.customIcon)\"></span>\n </button>\n </ng-container>\n </div>\n </ng-container>\n\n <!-- Link Cell -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'link'\">\n <a\n [href]=\"getLinkHref(getCellValue(row, column.key))\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pecb-table__link\"\n (click)=\"$event.stopPropagation()\">\n {{ getCellValue(row, column.key) }}\n </a>\n </ng-container>\n\n <!-- Default Text -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'text'\">\n {{ getCellValue(row, column.key) }}\n </ng-container>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination (outside table container) -->\n <div *ngIf=\"showPagination() && !loading() && data().length > 0\" [class]=\"paginationClasses\">\n <pecb-pagination\n [totalItems]=\"totalItems()\"\n [pageSize]=\"pageSize()\"\n [currentPage]=\"currentPage()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n [showPageSizeSelector]=\"showPageSizeSelector()\"\n [variant]=\"paginationVariant()\"\n size=\"md\"\n (pageChange)=\"onPageChange($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\">\n </pecb-pagination>\n </div>\n</div>\n", styles: [".pecb-table-wrapper{width:100%}.pecb-table-container{width:100%;background-color:#fff;border-radius:12px;overflow-x:auto;overflow-y:hidden;border:1px solid #ececec}.pecb-table-container--sticky{position:relative;overflow:auto}.pecb-table{width:100%;border-collapse:collapse;border-spacing:0;font-family:inherit;font-size:14px;color:#313131}.pecb-table--sm{font-size:12px}.pecb-table--sm .pecb-table__cell,.pecb-table--sm .pecb-table__cell--header{padding:8px 12px}.pecb-table--md{font-size:14px}.pecb-table--md .pecb-table__cell{padding:16px 18px}.pecb-table--md .pecb-table__cell--header{padding:12px 18px}.pecb-table--lg{font-size:16px}.pecb-table--lg .pecb-table__cell{padding:18px 20px}.pecb-table--lg .pecb-table__cell--header{padding:16px 20px}.pecb-table--default .pecb-table__row--data{border-bottom:1px solid #ececec}.pecb-table--default .pecb-table__row--data:last-child{border-bottom:none}.pecb-table--default .pecb-table__row--data:last-child .pecb-table__cell{border-bottom:none}.pecb-table--default .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--striped .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--bordered .pecb-table__cell{border:1px solid #f7f5f5}.pecb-table--hoverable .pecb-table__row--data{transition:background-color .15s ease-in-out}.pecb-table--selectable .pecb-table__row--data{cursor:pointer}.pecb-table__head{background-color:#212427}.pecb-table__head--sticky{position:sticky;top:0;z-index:1020}.pecb-table__row--header{text-transform:uppercase;height:52px}.pecb-table__row--data{background-color:#fff;height:58px}.pecb-table__row--data:focus,.pecb-table__row--data:focus-visible{outline:none}.pecb-table__row--selected{background-color:#f4f4f4}.pecb-table__row--selected:hover{background-color:#f7f5f5}.pecb-table__row--empty{background-color:#fff}.pecb-table__cell{text-align:left;vertical-align:middle;white-space:nowrap;border-bottom:1px solid #ececec}.pecb-table__cell--header{font-weight:500;font-size:12px;color:#fff;letter-spacing:.02em;-webkit-user-select:none;user-select:none;border-bottom:none}.pecb-table__cell--sortable{cursor:pointer;transition:background-color .15s ease-in-out}.pecb-table__cell--sortable:hover{background-color:#ffffff1a}.pecb-table__cell--sortable:focus,.pecb-table__cell--sortable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px #fff}.pecb-table__cell--checkbox{width:48px;text-align:center}.pecb-table__cell--center{text-align:center}.pecb-table__cell--right{text-align:right}.pecb-table__cell--empty{text-align:center;padding:24px 0}.pecb-table__cell--truncate{overflow:hidden;text-overflow:ellipsis}.pecb-table__cell--wrap{white-space:normal;overflow-wrap:anywhere;word-break:break-word}.pecb-table__header-content{display:flex;align-items:center;gap:8px}.pecb-table__header-text{flex:1}.pecb-table__sort-icon{display:flex;align-items:center;justify-content:center;color:#ffffff80;transition:color .15s ease-in-out}.pecb-table__sort-icon svg{width:16px;height:16px}.pecb-table__sort-icon[data-sort=asc],.pecb-table__sort-icon[data-sort=desc]{color:#fff}.pecb-table__checkbox-wrapper{display:inline-flex;align-items:center;justify-content:center;position:relative;cursor:pointer}.pecb-table__checkbox{position:absolute;opacity:0;width:0;height:0}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom:after{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:before{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:after{display:none}.pecb-table__checkbox:focus+.pecb-table__checkbox-custom{box-shadow:0 0 0 2px #21242733}.pecb-table__checkbox-custom{width:18px;height:18px;border:1.5px solid #c2c2c2;border-radius:6px;background-color:#fff;position:relative;transition:all .15s ease-in-out}.pecb-table__checkbox-custom:after{content:\"\";display:none;position:absolute;left:5px;top:2px;width:4px;height:8px;border:solid #ffffff;border-width:0 2px 2px 0;transform:rotate(45deg)}.pecb-table__checkbox-custom:before{content:\"\";display:none;position:absolute;left:3px;top:6px;width:8px;height:2px;background-color:#fff}.pecb-table__empty-content{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 0}.pecb-table__empty-message{color:#919191;font-size:14px}.pecb-table__loading{display:flex;align-items:center;justify-content:center;padding:48px;min-height:200px}.pecb-table__loading-spinner{display:flex;flex-direction:column;align-items:center;gap:12px;color:#616161;font-size:14px;font-family:inherit;font-weight:400}.pecb-table__spinner-icon{width:32px;height:32px;animation:spin 1s linear infinite;color:#212427}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pecb-table__pagination{padding:16px 0;margin-top:16px}.pecb-table__pagination--bordered{padding:16px 18px;background-color:#fff;border:1px solid #ececec;border-radius:6px}.pecb-table__user-cell{display:flex;align-items:center;gap:12px}.pecb-table__user-avatar{width:40px;height:40px;border-radius:9999px;object-fit:cover;flex-shrink:0}.pecb-table__user-info{display:flex;flex-direction:column;min-width:0}.pecb-table__user-name{font-weight:500;color:#313131;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__user-secondary{font-size:12px;color:#919191;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__action-buttons{display:flex;align-items:center;gap:0;justify-content:center}.pecb-table__action-divider{display:block;width:1px;height:16px;background-color:#c2c2c2;flex-shrink:0}.pecb-table__action-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#212427;cursor:pointer;border-radius:6px;transition:all .15s ease-in-out;padding:0}.pecb-table__action-btn:hover{background-color:#f7f5f5;color:#313131}.pecb-table__action-btn:focus{outline:none;box-shadow:0 0 0 2px #21242733}.pecb-table__action-btn:disabled{opacity:.5;cursor:not-allowed}.pecb-table__action-btn--danger:hover{background-color:#fdecea;color:#dc3545}.pecb-table__action-btn svg{width:18px;height:18px}.pecb-table__custom-icon{display:flex;align-items:center;justify-content:center}.pecb-table__custom-icon svg{width:18px;height:18px}.pecb-table__link{color:#212427;text-decoration:none;font-size:inherit}.pecb-table__link:hover{text-decoration:underline;color:#313131}.pecb-table__link:focus-visible{outline:2px solid #212427;outline-offset:2px;border-radius:2px}@media (min-width: 768px){.pecb-table--md,.pecb-table--lg{font-size:14px}.pecb-table--md .pecb-table__cell,.pecb-table--lg .pecb-table__cell{padding:12px 16px}.pecb-table--md .pecb-table__cell--header,.pecb-table--lg .pecb-table__cell--header{padding:8px 16px}.pecb-table__pagination{padding:12px 16px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: PaginationComponent, selector: "pecb-pagination", inputs: ["totalItems", "pageSize", "currentPage", "pageSizeOptions", "size", "variant", "showPageSizeSelector", "showItemsInfo", "maxVisiblePages", "disabled", "aria-label", "previousPageAriaLabel", "nextPageAriaLabel", "pageNavigationAriaLabel", "pageAriaLabel", "pageSizeListboxAriaLabel", "itemsPerPageLabel", "itemsInfoTemplate", "itemsInfoCompactTemplate"], outputs: ["pageSizeChange", "currentPageChange", "pageChange"] }, { kind: "component", type: StatusComponent, selector: "pecb-status", inputs: ["type", "variant", "size", "ariaLabel", "text", "customClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4528
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: TableComponent, isStandalone: true, selector: "pecb-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, hoverable: { classPropertyName: "hoverable", publicName: "hoverable", isSignal: true, isRequired: false, transformFunction: null }, showPagination: { classPropertyName: "showPagination", publicName: "showPagination", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, showPageSizeSelector: { classPropertyName: "showPageSizeSelector", publicName: "showPageSizeSelector", isSignal: true, isRequired: false, transformFunction: null }, paginationVariant: { classPropertyName: "paginationVariant", publicName: "paginationVariant", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, selectedIndices: { classPropertyName: "selectedIndices", publicName: "selectedIndices", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, wrapText: { classPropertyName: "wrapText", publicName: "wrapText", isSignal: true, isRequired: false, transformFunction: null }, sortColumn: { classPropertyName: "sortColumn", publicName: "sortColumn", isSignal: true, isRequired: false, transformFunction: null }, sortDirection: { classPropertyName: "sortDirection", publicName: "sortDirection", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, selectAllAriaLabel: { classPropertyName: "selectAllAriaLabel", publicName: "selectAllAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, selectRowAriaLabel: { classPropertyName: "selectRowAriaLabel", publicName: "selectRowAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", pageSize: "pageSizeChange", selectedIndices: "selectedIndicesChange", sortColumn: "sortColumnChange", sortDirection: "sortDirectionChange", pageChange: "pageChange", rowClick: "rowClick", rowAction: "rowAction", sortChange: "sortChange", selectionChange: "selectionChange" }, queries: [{ propertyName: "emptyTemplate", first: true, predicate: ["emptyTemplate"], descendants: true }, { propertyName: "loadingTemplate", first: true, predicate: ["loadingTemplate"], descendants: true }], ngImport: i0, template: "<div class=\"pecb-table-wrapper\">\n <div [class]=\"containerClasses\" [style.maxHeight]=\"maxHeight()\">\n <!-- Loading State -->\n <div *ngIf=\"loading()\" class=\"pecb-table__loading\">\n <ng-container *ngIf=\"loadingTemplate; else defaultLoading\">\n <ng-container *ngTemplateOutlet=\"loadingTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultLoading>\n <div class=\"pecb-table__loading-spinner\">\n <svg class=\"pecb-table__spinner-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-dasharray=\"31.4 31.4\" />\n </svg>\n <span>{{ loadingText() }}</span>\n </div>\n </ng-template>\n </div>\n\n <!-- Table -->\n <table\n *ngIf=\"!loading()\"\n [class]=\"tableClasses\"\n [style.min-width]=\"minWidth()\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"table\">\n\n <!-- Table Header -->\n <thead *ngIf=\"showHeader()\" class=\"pecb-table__head\" [class.pecb-table__head--sticky]=\"stickyHeader()\">\n <tr class=\"pecb-table__row pecb-table__row--header\" role=\"row\">\n <!-- Selection Checkbox Column -->\n <th\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--header pecb-table__cell--checkbox\"\n role=\"columnheader\"\n scope=\"col\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isSomeSelected()\"\n (change)=\"toggleAllSelection()\"\n [attr.aria-label]=\"selectAllAriaLabel()\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </th>\n\n <!-- Data Columns -->\n <th\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell pecb-table__cell--header\"\n [class.pecb-table__cell--sortable]=\"column.sortable\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [style.width]=\"column.width\"\n [attr.aria-sort]=\"column.sortable ? (sortColumn() === column.key ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : 'none') : null\"\n role=\"columnheader\"\n scope=\"col\"\n (click)=\"onHeaderClick(column)\"\n (keydown.enter)=\"onHeaderClick(column)\"\n (keydown.space)=\"onHeaderClick(column); $event.preventDefault()\"\n [attr.tabindex]=\"column.sortable ? 0 : null\">\n <div class=\"pecb-table__header-content\">\n <ng-container *ngIf=\"column.headerTemplate; else defaultHeader\">\n <ng-container *ngTemplateOutlet=\"column.headerTemplate; context: { $implicit: column }\"></ng-container>\n </ng-container>\n <ng-template #defaultHeader>\n <span class=\"pecb-table__header-text\">{{ column.label }}</span>\n </ng-template>\n\n <!-- Sort Icon -->\n <span *ngIf=\"column.sortable\" class=\"pecb-table__sort-icon\" [attr.data-sort]=\"getSortState(column)\">\n <svg *ngIf=\"getSortState(column) === 'none'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'asc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'desc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\"/>\n </svg>\n </span>\n </div>\n </th>\n </tr>\n </thead>\n\n <!-- Table Body -->\n <tbody class=\"pecb-table__body\">\n <!-- Empty State -->\n <tr *ngIf=\"data().length === 0\" class=\"pecb-table__row pecb-table__row--empty\">\n <td\n [attr.colspan]=\"selectable() ? columns().length + 1 : columns().length\"\n class=\"pecb-table__cell pecb-table__cell--empty\">\n <ng-container *ngIf=\"emptyTemplate; else defaultEmpty\">\n <ng-container *ngTemplateOutlet=\"emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"pecb-table__empty-content\">\n <span class=\"pecb-table__empty-message\">{{ emptyMessage() }}</span>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data Rows -->\n <tr\n *ngFor=\"let row of data(); let i = index; trackBy: trackByIndex\"\n class=\"pecb-table__row pecb-table__row--data\"\n [class.pecb-table__row--selected]=\"isRowSelected(i)\"\n role=\"row\"\n (click)=\"onRowClick(row, i)\"\n [attr.tabindex]=\"0\"\n (keydown.enter)=\"onRowClick(row, i)\">\n\n <!-- Selection Checkbox -->\n <td\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--checkbox\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isRowSelected(i)\"\n (change)=\"toggleRowSelection(i)\"\n [attr.aria-label]=\"selectRowAriaLabel() + ' ' + (i + 1)\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </td>\n\n <!-- Data Cells -->\n <td\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [class.pecb-table__cell--wrap]=\"isCellWrapped(column)\"\n [class.pecb-table__cell--truncate]=\"!isCellWrapped(column)\"\n [style.max-width]=\"column.maxWidth || column.width\"\n [attr.title]=\"getCellTitle(row, column)\"\n role=\"cell\">\n\n <!-- Custom Template -->\n <ng-container *ngIf=\"column.cellTemplate\">\n <ng-container *ngTemplateOutlet=\"column.cellTemplate; context: { $implicit: getCellValue(row, column.key), row: row, column: column, index: i }\"></ng-container>\n </ng-container>\n\n <!-- Dynamic Component - renders any component passed via dynamicComponent -->\n <ng-container *ngIf=\"!column.cellTemplate && hasDynamicComponent(column)\">\n <ng-container\n *ngComponentOutlet=\"column.dynamicComponent!; inputs: getDynamicComponentInputs(row, column, i)\">\n </ng-container>\n </ng-container>\n\n <!-- Status Component - renders pecb-status -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'status'\">\n <pecb-status\n [type]=\"getStatusType(getCellValue(row, column.key), column)\"\n [variant]=\"getStatusVariant(column)\"\n [size]=\"getStatusSize(column)\">\n {{ getCellValue(row, column.key) }}\n </pecb-status>\n </ng-container>\n\n <!-- User Component - renders avatar + name + email -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'user'\">\n <div class=\"pecb-table__user-cell\">\n <img\n *ngIf=\"getUserData(row, column).avatar\"\n [src]=\"getUserData(row, column).avatar\"\n alt=\"\"\n class=\"pecb-table__user-avatar\">\n <div class=\"pecb-table__user-info\">\n <div class=\"pecb-table__user-name\">{{ getUserData(row, column).name }}</div>\n <div *ngIf=\"getUserData(row, column).secondary\" class=\"pecb-table__user-secondary\">\n {{ getUserData(row, column).secondary }}\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Action Component - renders action buttons with icons -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'action'\">\n <div class=\"pecb-table__action-buttons\">\n <ng-container *ngFor=\"let action of getActions(column)\">\n <!-- Divider before this action (consumer-controlled grouping) -->\n <span *ngIf=\"action.dividerBefore\" class=\"pecb-table__action-divider\"></span>\n <button\n class=\"pecb-table__action-btn\"\n [class.pecb-table__action-btn--danger]=\"action.variant === 'danger'\"\n [title]=\"action.tooltip || action.id\"\n [disabled]=\"action.disabled\"\n (click)=\"onActionClick(action, row, i, $event)\">\n <!-- View Icon -->\n <svg *ngIf=\"action.icon === 'view'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"></path>\n <circle cx=\"12\" cy=\"12\" r=\"3\"></circle>\n </svg>\n <!-- Edit Icon -->\n <svg *ngIf=\"action.icon === 'edit'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"></path>\n <path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"></path>\n </svg>\n <!-- Delete Icon -->\n <svg *ngIf=\"action.icon === 'delete'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"></polyline>\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path>\n </svg>\n <!-- Download Icon -->\n <svg *ngIf=\"action.icon === 'download'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path>\n <polyline points=\"7 10 12 15 17 10\"></polyline>\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line>\n </svg>\n <!-- Copy Icon -->\n <svg *ngIf=\"action.icon === 'copy'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n </svg>\n <!-- Custom Icon (SVG string) -->\n <span *ngIf=\"action.icon === 'custom' && action.customIcon\" class=\"pecb-table__custom-icon\" [innerHTML]=\"getSafeIcon(action.customIcon)\"></span>\n </button>\n </ng-container>\n </div>\n </ng-container>\n\n <!-- Link Cell -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'link'\">\n <a\n [href]=\"getLinkHref(getCellValue(row, column.key))\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pecb-table__link\"\n (click)=\"$event.stopPropagation()\">\n {{ getCellValue(row, column.key) }}\n </a>\n </ng-container>\n\n <!-- Default Text -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'text'\">\n {{ getCellValue(row, column.key) }}\n </ng-container>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination (outside table container) -->\n <div *ngIf=\"showPagination() && !loading() && data().length > 0\" [class]=\"paginationClasses\">\n <pecb-pagination\n [totalItems]=\"totalItems()\"\n [pageSize]=\"pageSize()\"\n [currentPage]=\"currentPage()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n [showPageSizeSelector]=\"showPageSizeSelector()\"\n [variant]=\"paginationVariant()\"\n size=\"md\"\n (pageChange)=\"onPageChange($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\">\n </pecb-pagination>\n </div>\n</div>\n", styles: [".pecb-table-wrapper{width:100%}.pecb-table-container{width:100%;background-color:#fff;border-radius:12px;overflow-x:auto;overflow-y:hidden;border:1px solid #ececec}.pecb-table-container--sticky{position:relative;overflow:auto}.pecb-table{width:100%;border-collapse:collapse;border-spacing:0;font-family:inherit;font-size:14px;color:#313131}.pecb-table--sm{font-size:12px}.pecb-table--sm .pecb-table__cell,.pecb-table--sm .pecb-table__cell--header{padding:8px 12px}.pecb-table--md{font-size:14px}.pecb-table--md .pecb-table__cell{padding:16px 18px}.pecb-table--md .pecb-table__cell--header{padding:12px 18px}.pecb-table--lg{font-size:16px}.pecb-table--lg .pecb-table__cell{padding:18px 20px}.pecb-table--lg .pecb-table__cell--header{padding:16px 20px}.pecb-table--default .pecb-table__row--data{border-bottom:1px solid #ececec}.pecb-table--default .pecb-table__row--data:last-child{border-bottom:none}.pecb-table--default .pecb-table__row--data:last-child .pecb-table__cell{border-bottom:none}.pecb-table--default .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--striped .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--bordered .pecb-table__cell{border:1px solid #f7f5f5}.pecb-table--hoverable .pecb-table__row--data{transition:background-color .15s ease-in-out}.pecb-table--selectable .pecb-table__row--data{cursor:pointer}.pecb-table__head{background-color:#212427}.pecb-table__head--sticky{position:sticky;top:0;z-index:1020}.pecb-table__row--header{text-transform:uppercase;height:52px}.pecb-table__row--data{background-color:#fff;height:58px}.pecb-table__row--data:focus,.pecb-table__row--data:focus-visible{outline:none}.pecb-table__row--selected{background-color:#f4f4f4}.pecb-table__row--selected:hover{background-color:#f7f5f5}.pecb-table__row--empty{background-color:#fff}.pecb-table__cell{text-align:left;vertical-align:middle;white-space:nowrap;border-bottom:1px solid #ececec}.pecb-table__cell--header{font-weight:500;font-size:12px;color:#fff;letter-spacing:.02em;-webkit-user-select:none;user-select:none;border-bottom:none;background-color:#212427}.pecb-table__cell--sortable{cursor:pointer;transition:background-color .15s ease-in-out}.pecb-table__cell--sortable:hover{background-color:#373a3d}.pecb-table__cell--sortable:focus,.pecb-table__cell--sortable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px #fff}.pecb-table__cell--checkbox{width:48px;text-align:center}.pecb-table__cell--center{text-align:center}.pecb-table__cell--right{text-align:right}.pecb-table__cell--empty{text-align:center;padding:24px 0}.pecb-table__cell--truncate{overflow:hidden;text-overflow:ellipsis}.pecb-table__cell--wrap{white-space:normal;overflow-wrap:anywhere;word-break:break-word}.pecb-table__header-content{display:flex;align-items:center;gap:8px}.pecb-table__header-text{flex:1}.pecb-table__sort-icon{display:flex;align-items:center;justify-content:center;color:#ffffff80;transition:color .15s ease-in-out}.pecb-table__sort-icon svg{width:16px;height:16px}.pecb-table__sort-icon[data-sort=asc],.pecb-table__sort-icon[data-sort=desc]{color:#fff}.pecb-table__checkbox-wrapper{display:inline-flex;align-items:center;justify-content:center;position:relative;cursor:pointer}.pecb-table__checkbox{position:absolute;opacity:0;width:0;height:0}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom:after{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:before{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:after{display:none}.pecb-table__checkbox:focus+.pecb-table__checkbox-custom{box-shadow:0 0 0 2px #21242733}.pecb-table__checkbox-custom{width:18px;height:18px;border:1.5px solid #c2c2c2;border-radius:6px;background-color:#fff;position:relative;transition:all .15s ease-in-out}.pecb-table__checkbox-custom:after{content:\"\";display:none;position:absolute;left:5px;top:2px;width:4px;height:8px;border:solid #ffffff;border-width:0 2px 2px 0;transform:rotate(45deg)}.pecb-table__checkbox-custom:before{content:\"\";display:none;position:absolute;left:3px;top:6px;width:8px;height:2px;background-color:#fff}.pecb-table__empty-content{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 0}.pecb-table__empty-message{color:#919191;font-size:14px}.pecb-table__loading{display:flex;align-items:center;justify-content:center;padding:48px;min-height:200px}.pecb-table__loading-spinner{display:flex;flex-direction:column;align-items:center;gap:12px;color:#616161;font-size:14px;font-family:inherit;font-weight:400}.pecb-table__spinner-icon{width:32px;height:32px;animation:spin 1s linear infinite;color:#212427}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pecb-table__pagination{padding:16px 0;margin-top:16px}.pecb-table__pagination--bordered{padding:16px 18px;background-color:#fff;border:1px solid #ececec;border-radius:6px}.pecb-table__user-cell{display:flex;align-items:center;gap:12px}.pecb-table__user-avatar{width:40px;height:40px;border-radius:9999px;object-fit:cover;flex-shrink:0}.pecb-table__user-info{display:flex;flex-direction:column;min-width:0}.pecb-table__user-name{font-weight:500;color:#313131;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__user-secondary{font-size:12px;color:#919191;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__action-buttons{display:flex;align-items:center;gap:0;justify-content:center}.pecb-table__action-divider{display:block;width:1px;height:16px;background-color:#c2c2c2;flex-shrink:0}.pecb-table__action-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#212427;cursor:pointer;border-radius:6px;transition:all .15s ease-in-out;padding:0}.pecb-table__action-btn:hover{background-color:#f7f5f5;color:#313131}.pecb-table__action-btn:focus{outline:none;box-shadow:0 0 0 2px #21242733}.pecb-table__action-btn:disabled{opacity:.5;cursor:not-allowed}.pecb-table__action-btn--danger:hover{background-color:#fdecea;color:#dc3545}.pecb-table__action-btn svg{width:18px;height:18px}.pecb-table__custom-icon{display:flex;align-items:center;justify-content:center}.pecb-table__custom-icon svg{width:18px;height:18px}.pecb-table__link{color:#212427;text-decoration:none;font-size:inherit}.pecb-table__link:hover{text-decoration:underline;color:#313131}.pecb-table__link:focus-visible{outline:2px solid #212427;outline-offset:2px;border-radius:2px}@media (min-width: 768px){.pecb-table--md,.pecb-table--lg{font-size:14px}.pecb-table--md .pecb-table__cell,.pecb-table--lg .pecb-table__cell{padding:12px 16px}.pecb-table--md .pecb-table__cell--header,.pecb-table--lg .pecb-table__cell--header{padding:8px 16px}.pecb-table__pagination{padding:12px 16px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: PaginationComponent, selector: "pecb-pagination", inputs: ["totalItems", "pageSize", "currentPage", "pageSizeOptions", "size", "variant", "showPageSizeSelector", "showItemsInfo", "maxVisiblePages", "disabled", "aria-label", "previousPageAriaLabel", "nextPageAriaLabel", "pageNavigationAriaLabel", "pageAriaLabel", "pageSizeListboxAriaLabel", "itemsPerPageLabel", "itemsInfoTemplate", "itemsInfoCompactTemplate"], outputs: ["pageSizeChange", "currentPageChange", "pageChange"] }, { kind: "component", type: StatusComponent, selector: "pecb-status", inputs: ["type", "variant", "size", "ariaLabel", "text", "customClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4529
4529
  }
4530
4530
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: TableComponent, decorators: [{
4531
4531
  type: Component,
4532
- args: [{ selector: 'pecb-table', imports: [CommonModule, PaginationComponent, StatusComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"pecb-table-wrapper\">\n <div [class]=\"containerClasses\" [style.maxHeight]=\"maxHeight()\">\n <!-- Loading State -->\n <div *ngIf=\"loading()\" class=\"pecb-table__loading\">\n <ng-container *ngIf=\"loadingTemplate; else defaultLoading\">\n <ng-container *ngTemplateOutlet=\"loadingTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultLoading>\n <div class=\"pecb-table__loading-spinner\">\n <svg class=\"pecb-table__spinner-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-dasharray=\"31.4 31.4\" />\n </svg>\n <span>{{ loadingText() }}</span>\n </div>\n </ng-template>\n </div>\n\n <!-- Table -->\n <table\n *ngIf=\"!loading()\"\n [class]=\"tableClasses\"\n [style.min-width]=\"minWidth()\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"table\">\n\n <!-- Table Header -->\n <thead *ngIf=\"showHeader()\" class=\"pecb-table__head\" [class.pecb-table__head--sticky]=\"stickyHeader()\">\n <tr class=\"pecb-table__row pecb-table__row--header\" role=\"row\">\n <!-- Selection Checkbox Column -->\n <th\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--header pecb-table__cell--checkbox\"\n role=\"columnheader\"\n scope=\"col\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isSomeSelected()\"\n (change)=\"toggleAllSelection()\"\n [attr.aria-label]=\"selectAllAriaLabel()\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </th>\n\n <!-- Data Columns -->\n <th\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell pecb-table__cell--header\"\n [class.pecb-table__cell--sortable]=\"column.sortable\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [style.width]=\"column.width\"\n [attr.aria-sort]=\"column.sortable ? (sortColumn() === column.key ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : 'none') : null\"\n role=\"columnheader\"\n scope=\"col\"\n (click)=\"onHeaderClick(column)\"\n (keydown.enter)=\"onHeaderClick(column)\"\n (keydown.space)=\"onHeaderClick(column); $event.preventDefault()\"\n [attr.tabindex]=\"column.sortable ? 0 : null\">\n <div class=\"pecb-table__header-content\">\n <ng-container *ngIf=\"column.headerTemplate; else defaultHeader\">\n <ng-container *ngTemplateOutlet=\"column.headerTemplate; context: { $implicit: column }\"></ng-container>\n </ng-container>\n <ng-template #defaultHeader>\n <span class=\"pecb-table__header-text\">{{ column.label }}</span>\n </ng-template>\n\n <!-- Sort Icon -->\n <span *ngIf=\"column.sortable\" class=\"pecb-table__sort-icon\" [attr.data-sort]=\"getSortState(column)\">\n <svg *ngIf=\"getSortState(column) === 'none'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'asc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'desc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\"/>\n </svg>\n </span>\n </div>\n </th>\n </tr>\n </thead>\n\n <!-- Table Body -->\n <tbody class=\"pecb-table__body\">\n <!-- Empty State -->\n <tr *ngIf=\"data().length === 0\" class=\"pecb-table__row pecb-table__row--empty\">\n <td\n [attr.colspan]=\"selectable() ? columns().length + 1 : columns().length\"\n class=\"pecb-table__cell pecb-table__cell--empty\">\n <ng-container *ngIf=\"emptyTemplate; else defaultEmpty\">\n <ng-container *ngTemplateOutlet=\"emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"pecb-table__empty-content\">\n <span class=\"pecb-table__empty-message\">{{ emptyMessage() }}</span>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data Rows -->\n <tr\n *ngFor=\"let row of data(); let i = index; trackBy: trackByIndex\"\n class=\"pecb-table__row pecb-table__row--data\"\n [class.pecb-table__row--selected]=\"isRowSelected(i)\"\n role=\"row\"\n (click)=\"onRowClick(row, i)\"\n [attr.tabindex]=\"0\"\n (keydown.enter)=\"onRowClick(row, i)\">\n\n <!-- Selection Checkbox -->\n <td\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--checkbox\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isRowSelected(i)\"\n (change)=\"toggleRowSelection(i)\"\n [attr.aria-label]=\"selectRowAriaLabel() + ' ' + (i + 1)\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </td>\n\n <!-- Data Cells -->\n <td\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [class.pecb-table__cell--wrap]=\"isCellWrapped(column)\"\n [class.pecb-table__cell--truncate]=\"!isCellWrapped(column)\"\n [style.max-width]=\"column.maxWidth || column.width\"\n [attr.title]=\"getCellTitle(row, column)\"\n role=\"cell\">\n\n <!-- Custom Template -->\n <ng-container *ngIf=\"column.cellTemplate\">\n <ng-container *ngTemplateOutlet=\"column.cellTemplate; context: { $implicit: getCellValue(row, column.key), row: row, column: column, index: i }\"></ng-container>\n </ng-container>\n\n <!-- Dynamic Component - renders any component passed via dynamicComponent -->\n <ng-container *ngIf=\"!column.cellTemplate && hasDynamicComponent(column)\">\n <ng-container\n *ngComponentOutlet=\"column.dynamicComponent!; inputs: getDynamicComponentInputs(row, column, i)\">\n </ng-container>\n </ng-container>\n\n <!-- Status Component - renders pecb-status -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'status'\">\n <pecb-status\n [type]=\"getStatusType(getCellValue(row, column.key), column)\"\n [variant]=\"getStatusVariant(column)\"\n [size]=\"getStatusSize(column)\">\n {{ getCellValue(row, column.key) }}\n </pecb-status>\n </ng-container>\n\n <!-- User Component - renders avatar + name + email -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'user'\">\n <div class=\"pecb-table__user-cell\">\n <img\n *ngIf=\"getUserData(row, column).avatar\"\n [src]=\"getUserData(row, column).avatar\"\n alt=\"\"\n class=\"pecb-table__user-avatar\">\n <div class=\"pecb-table__user-info\">\n <div class=\"pecb-table__user-name\">{{ getUserData(row, column).name }}</div>\n <div *ngIf=\"getUserData(row, column).secondary\" class=\"pecb-table__user-secondary\">\n {{ getUserData(row, column).secondary }}\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Action Component - renders action buttons with icons -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'action'\">\n <div class=\"pecb-table__action-buttons\">\n <ng-container *ngFor=\"let action of getActions(column)\">\n <!-- Divider before this action (consumer-controlled grouping) -->\n <span *ngIf=\"action.dividerBefore\" class=\"pecb-table__action-divider\"></span>\n <button\n class=\"pecb-table__action-btn\"\n [class.pecb-table__action-btn--danger]=\"action.variant === 'danger'\"\n [title]=\"action.tooltip || action.id\"\n [disabled]=\"action.disabled\"\n (click)=\"onActionClick(action, row, i, $event)\">\n <!-- View Icon -->\n <svg *ngIf=\"action.icon === 'view'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"></path>\n <circle cx=\"12\" cy=\"12\" r=\"3\"></circle>\n </svg>\n <!-- Edit Icon -->\n <svg *ngIf=\"action.icon === 'edit'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"></path>\n <path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"></path>\n </svg>\n <!-- Delete Icon -->\n <svg *ngIf=\"action.icon === 'delete'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"></polyline>\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path>\n </svg>\n <!-- Download Icon -->\n <svg *ngIf=\"action.icon === 'download'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path>\n <polyline points=\"7 10 12 15 17 10\"></polyline>\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line>\n </svg>\n <!-- Copy Icon -->\n <svg *ngIf=\"action.icon === 'copy'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n </svg>\n <!-- Custom Icon (SVG string) -->\n <span *ngIf=\"action.icon === 'custom' && action.customIcon\" class=\"pecb-table__custom-icon\" [innerHTML]=\"getSafeIcon(action.customIcon)\"></span>\n </button>\n </ng-container>\n </div>\n </ng-container>\n\n <!-- Link Cell -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'link'\">\n <a\n [href]=\"getLinkHref(getCellValue(row, column.key))\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pecb-table__link\"\n (click)=\"$event.stopPropagation()\">\n {{ getCellValue(row, column.key) }}\n </a>\n </ng-container>\n\n <!-- Default Text -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'text'\">\n {{ getCellValue(row, column.key) }}\n </ng-container>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination (outside table container) -->\n <div *ngIf=\"showPagination() && !loading() && data().length > 0\" [class]=\"paginationClasses\">\n <pecb-pagination\n [totalItems]=\"totalItems()\"\n [pageSize]=\"pageSize()\"\n [currentPage]=\"currentPage()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n [showPageSizeSelector]=\"showPageSizeSelector()\"\n [variant]=\"paginationVariant()\"\n size=\"md\"\n (pageChange)=\"onPageChange($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\">\n </pecb-pagination>\n </div>\n</div>\n", styles: [".pecb-table-wrapper{width:100%}.pecb-table-container{width:100%;background-color:#fff;border-radius:12px;overflow-x:auto;overflow-y:hidden;border:1px solid #ececec}.pecb-table-container--sticky{position:relative;overflow:auto}.pecb-table{width:100%;border-collapse:collapse;border-spacing:0;font-family:inherit;font-size:14px;color:#313131}.pecb-table--sm{font-size:12px}.pecb-table--sm .pecb-table__cell,.pecb-table--sm .pecb-table__cell--header{padding:8px 12px}.pecb-table--md{font-size:14px}.pecb-table--md .pecb-table__cell{padding:16px 18px}.pecb-table--md .pecb-table__cell--header{padding:12px 18px}.pecb-table--lg{font-size:16px}.pecb-table--lg .pecb-table__cell{padding:18px 20px}.pecb-table--lg .pecb-table__cell--header{padding:16px 20px}.pecb-table--default .pecb-table__row--data{border-bottom:1px solid #ececec}.pecb-table--default .pecb-table__row--data:last-child{border-bottom:none}.pecb-table--default .pecb-table__row--data:last-child .pecb-table__cell{border-bottom:none}.pecb-table--default .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--striped .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--bordered .pecb-table__cell{border:1px solid #f7f5f5}.pecb-table--hoverable .pecb-table__row--data{transition:background-color .15s ease-in-out}.pecb-table--selectable .pecb-table__row--data{cursor:pointer}.pecb-table__head{background-color:#212427}.pecb-table__head--sticky{position:sticky;top:0;z-index:1020}.pecb-table__row--header{text-transform:uppercase;height:52px}.pecb-table__row--data{background-color:#fff;height:58px}.pecb-table__row--data:focus,.pecb-table__row--data:focus-visible{outline:none}.pecb-table__row--selected{background-color:#f4f4f4}.pecb-table__row--selected:hover{background-color:#f7f5f5}.pecb-table__row--empty{background-color:#fff}.pecb-table__cell{text-align:left;vertical-align:middle;white-space:nowrap;border-bottom:1px solid #ececec}.pecb-table__cell--header{font-weight:500;font-size:12px;color:#fff;letter-spacing:.02em;-webkit-user-select:none;user-select:none;border-bottom:none}.pecb-table__cell--sortable{cursor:pointer;transition:background-color .15s ease-in-out}.pecb-table__cell--sortable:hover{background-color:#ffffff1a}.pecb-table__cell--sortable:focus,.pecb-table__cell--sortable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px #fff}.pecb-table__cell--checkbox{width:48px;text-align:center}.pecb-table__cell--center{text-align:center}.pecb-table__cell--right{text-align:right}.pecb-table__cell--empty{text-align:center;padding:24px 0}.pecb-table__cell--truncate{overflow:hidden;text-overflow:ellipsis}.pecb-table__cell--wrap{white-space:normal;overflow-wrap:anywhere;word-break:break-word}.pecb-table__header-content{display:flex;align-items:center;gap:8px}.pecb-table__header-text{flex:1}.pecb-table__sort-icon{display:flex;align-items:center;justify-content:center;color:#ffffff80;transition:color .15s ease-in-out}.pecb-table__sort-icon svg{width:16px;height:16px}.pecb-table__sort-icon[data-sort=asc],.pecb-table__sort-icon[data-sort=desc]{color:#fff}.pecb-table__checkbox-wrapper{display:inline-flex;align-items:center;justify-content:center;position:relative;cursor:pointer}.pecb-table__checkbox{position:absolute;opacity:0;width:0;height:0}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom:after{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:before{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:after{display:none}.pecb-table__checkbox:focus+.pecb-table__checkbox-custom{box-shadow:0 0 0 2px #21242733}.pecb-table__checkbox-custom{width:18px;height:18px;border:1.5px solid #c2c2c2;border-radius:6px;background-color:#fff;position:relative;transition:all .15s ease-in-out}.pecb-table__checkbox-custom:after{content:\"\";display:none;position:absolute;left:5px;top:2px;width:4px;height:8px;border:solid #ffffff;border-width:0 2px 2px 0;transform:rotate(45deg)}.pecb-table__checkbox-custom:before{content:\"\";display:none;position:absolute;left:3px;top:6px;width:8px;height:2px;background-color:#fff}.pecb-table__empty-content{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 0}.pecb-table__empty-message{color:#919191;font-size:14px}.pecb-table__loading{display:flex;align-items:center;justify-content:center;padding:48px;min-height:200px}.pecb-table__loading-spinner{display:flex;flex-direction:column;align-items:center;gap:12px;color:#616161;font-size:14px;font-family:inherit;font-weight:400}.pecb-table__spinner-icon{width:32px;height:32px;animation:spin 1s linear infinite;color:#212427}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pecb-table__pagination{padding:16px 0;margin-top:16px}.pecb-table__pagination--bordered{padding:16px 18px;background-color:#fff;border:1px solid #ececec;border-radius:6px}.pecb-table__user-cell{display:flex;align-items:center;gap:12px}.pecb-table__user-avatar{width:40px;height:40px;border-radius:9999px;object-fit:cover;flex-shrink:0}.pecb-table__user-info{display:flex;flex-direction:column;min-width:0}.pecb-table__user-name{font-weight:500;color:#313131;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__user-secondary{font-size:12px;color:#919191;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__action-buttons{display:flex;align-items:center;gap:0;justify-content:center}.pecb-table__action-divider{display:block;width:1px;height:16px;background-color:#c2c2c2;flex-shrink:0}.pecb-table__action-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#212427;cursor:pointer;border-radius:6px;transition:all .15s ease-in-out;padding:0}.pecb-table__action-btn:hover{background-color:#f7f5f5;color:#313131}.pecb-table__action-btn:focus{outline:none;box-shadow:0 0 0 2px #21242733}.pecb-table__action-btn:disabled{opacity:.5;cursor:not-allowed}.pecb-table__action-btn--danger:hover{background-color:#fdecea;color:#dc3545}.pecb-table__action-btn svg{width:18px;height:18px}.pecb-table__custom-icon{display:flex;align-items:center;justify-content:center}.pecb-table__custom-icon svg{width:18px;height:18px}.pecb-table__link{color:#212427;text-decoration:none;font-size:inherit}.pecb-table__link:hover{text-decoration:underline;color:#313131}.pecb-table__link:focus-visible{outline:2px solid #212427;outline-offset:2px;border-radius:2px}@media (min-width: 768px){.pecb-table--md,.pecb-table--lg{font-size:14px}.pecb-table--md .pecb-table__cell,.pecb-table--lg .pecb-table__cell{padding:12px 16px}.pecb-table--md .pecb-table__cell--header,.pecb-table--lg .pecb-table__cell--header{padding:8px 16px}.pecb-table__pagination{padding:12px 16px}}\n"] }]
4532
+ args: [{ selector: 'pecb-table', imports: [CommonModule, PaginationComponent, StatusComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"pecb-table-wrapper\">\n <div [class]=\"containerClasses\" [style.maxHeight]=\"maxHeight()\">\n <!-- Loading State -->\n <div *ngIf=\"loading()\" class=\"pecb-table__loading\">\n <ng-container *ngIf=\"loadingTemplate; else defaultLoading\">\n <ng-container *ngTemplateOutlet=\"loadingTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultLoading>\n <div class=\"pecb-table__loading-spinner\">\n <svg class=\"pecb-table__spinner-icon\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-dasharray=\"31.4 31.4\" />\n </svg>\n <span>{{ loadingText() }}</span>\n </div>\n </ng-template>\n </div>\n\n <!-- Table -->\n <table\n *ngIf=\"!loading()\"\n [class]=\"tableClasses\"\n [style.min-width]=\"minWidth()\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"table\">\n\n <!-- Table Header -->\n <thead *ngIf=\"showHeader()\" class=\"pecb-table__head\" [class.pecb-table__head--sticky]=\"stickyHeader()\">\n <tr class=\"pecb-table__row pecb-table__row--header\" role=\"row\">\n <!-- Selection Checkbox Column -->\n <th\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--header pecb-table__cell--checkbox\"\n role=\"columnheader\"\n scope=\"col\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isSomeSelected()\"\n (change)=\"toggleAllSelection()\"\n [attr.aria-label]=\"selectAllAriaLabel()\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </th>\n\n <!-- Data Columns -->\n <th\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell pecb-table__cell--header\"\n [class.pecb-table__cell--sortable]=\"column.sortable\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [style.width]=\"column.width\"\n [attr.aria-sort]=\"column.sortable ? (sortColumn() === column.key ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : 'none') : null\"\n role=\"columnheader\"\n scope=\"col\"\n (click)=\"onHeaderClick(column)\"\n (keydown.enter)=\"onHeaderClick(column)\"\n (keydown.space)=\"onHeaderClick(column); $event.preventDefault()\"\n [attr.tabindex]=\"column.sortable ? 0 : null\">\n <div class=\"pecb-table__header-content\">\n <ng-container *ngIf=\"column.headerTemplate; else defaultHeader\">\n <ng-container *ngTemplateOutlet=\"column.headerTemplate; context: { $implicit: column }\"></ng-container>\n </ng-container>\n <ng-template #defaultHeader>\n <span class=\"pecb-table__header-text\">{{ column.label }}</span>\n </ng-template>\n\n <!-- Sort Icon -->\n <span *ngIf=\"column.sortable\" class=\"pecb-table__sort-icon\" [attr.data-sort]=\"getSortState(column)\">\n <svg *ngIf=\"getSortState(column) === 'none'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'asc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n </svg>\n <svg *ngIf=\"getSortState(column) === 'desc'\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M8 3L12 7H4L8 3Z\" fill=\"currentColor\" opacity=\"0.4\"/>\n <path d=\"M8 13L4 9H12L8 13Z\" fill=\"currentColor\"/>\n </svg>\n </span>\n </div>\n </th>\n </tr>\n </thead>\n\n <!-- Table Body -->\n <tbody class=\"pecb-table__body\">\n <!-- Empty State -->\n <tr *ngIf=\"data().length === 0\" class=\"pecb-table__row pecb-table__row--empty\">\n <td\n [attr.colspan]=\"selectable() ? columns().length + 1 : columns().length\"\n class=\"pecb-table__cell pecb-table__cell--empty\">\n <ng-container *ngIf=\"emptyTemplate; else defaultEmpty\">\n <ng-container *ngTemplateOutlet=\"emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"pecb-table__empty-content\">\n <span class=\"pecb-table__empty-message\">{{ emptyMessage() }}</span>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data Rows -->\n <tr\n *ngFor=\"let row of data(); let i = index; trackBy: trackByIndex\"\n class=\"pecb-table__row pecb-table__row--data\"\n [class.pecb-table__row--selected]=\"isRowSelected(i)\"\n role=\"row\"\n (click)=\"onRowClick(row, i)\"\n [attr.tabindex]=\"0\"\n (keydown.enter)=\"onRowClick(row, i)\">\n\n <!-- Selection Checkbox -->\n <td\n *ngIf=\"selectable()\"\n class=\"pecb-table__cell pecb-table__cell--checkbox\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\">\n <label class=\"pecb-table__checkbox-wrapper\">\n <input\n type=\"checkbox\"\n class=\"pecb-table__checkbox\"\n [checked]=\"isRowSelected(i)\"\n (change)=\"toggleRowSelection(i)\"\n [attr.aria-label]=\"selectRowAriaLabel() + ' ' + (i + 1)\" />\n <span class=\"pecb-table__checkbox-custom\"></span>\n </label>\n </td>\n\n <!-- Data Cells -->\n <td\n *ngFor=\"let column of columns(); trackBy: trackByKey\"\n class=\"pecb-table__cell\"\n [class]=\"getAlignmentClass(column)\"\n [ngClass]=\"column.cssClass\"\n [class.pecb-table__cell--wrap]=\"isCellWrapped(column)\"\n [class.pecb-table__cell--truncate]=\"!isCellWrapped(column)\"\n [style.max-width]=\"column.maxWidth || column.width\"\n [attr.title]=\"getCellTitle(row, column)\"\n role=\"cell\">\n\n <!-- Custom Template -->\n <ng-container *ngIf=\"column.cellTemplate\">\n <ng-container *ngTemplateOutlet=\"column.cellTemplate; context: { $implicit: getCellValue(row, column.key), row: row, column: column, index: i }\"></ng-container>\n </ng-container>\n\n <!-- Dynamic Component - renders any component passed via dynamicComponent -->\n <ng-container *ngIf=\"!column.cellTemplate && hasDynamicComponent(column)\">\n <ng-container\n *ngComponentOutlet=\"column.dynamicComponent!; inputs: getDynamicComponentInputs(row, column, i)\">\n </ng-container>\n </ng-container>\n\n <!-- Status Component - renders pecb-status -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'status'\">\n <pecb-status\n [type]=\"getStatusType(getCellValue(row, column.key), column)\"\n [variant]=\"getStatusVariant(column)\"\n [size]=\"getStatusSize(column)\">\n {{ getCellValue(row, column.key) }}\n </pecb-status>\n </ng-container>\n\n <!-- User Component - renders avatar + name + email -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'user'\">\n <div class=\"pecb-table__user-cell\">\n <img\n *ngIf=\"getUserData(row, column).avatar\"\n [src]=\"getUserData(row, column).avatar\"\n alt=\"\"\n class=\"pecb-table__user-avatar\">\n <div class=\"pecb-table__user-info\">\n <div class=\"pecb-table__user-name\">{{ getUserData(row, column).name }}</div>\n <div *ngIf=\"getUserData(row, column).secondary\" class=\"pecb-table__user-secondary\">\n {{ getUserData(row, column).secondary }}\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Action Component - renders action buttons with icons -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'action'\">\n <div class=\"pecb-table__action-buttons\">\n <ng-container *ngFor=\"let action of getActions(column)\">\n <!-- Divider before this action (consumer-controlled grouping) -->\n <span *ngIf=\"action.dividerBefore\" class=\"pecb-table__action-divider\"></span>\n <button\n class=\"pecb-table__action-btn\"\n [class.pecb-table__action-btn--danger]=\"action.variant === 'danger'\"\n [title]=\"action.tooltip || action.id\"\n [disabled]=\"action.disabled\"\n (click)=\"onActionClick(action, row, i, $event)\">\n <!-- View Icon -->\n <svg *ngIf=\"action.icon === 'view'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"></path>\n <circle cx=\"12\" cy=\"12\" r=\"3\"></circle>\n </svg>\n <!-- Edit Icon -->\n <svg *ngIf=\"action.icon === 'edit'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"></path>\n <path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"></path>\n </svg>\n <!-- Delete Icon -->\n <svg *ngIf=\"action.icon === 'delete'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"></polyline>\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path>\n </svg>\n <!-- Download Icon -->\n <svg *ngIf=\"action.icon === 'download'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path>\n <polyline points=\"7 10 12 15 17 10\"></polyline>\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line>\n </svg>\n <!-- Copy Icon -->\n <svg *ngIf=\"action.icon === 'copy'\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n </svg>\n <!-- Custom Icon (SVG string) -->\n <span *ngIf=\"action.icon === 'custom' && action.customIcon\" class=\"pecb-table__custom-icon\" [innerHTML]=\"getSafeIcon(action.customIcon)\"></span>\n </button>\n </ng-container>\n </div>\n </ng-container>\n\n <!-- Link Cell -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'link'\">\n <a\n [href]=\"getLinkHref(getCellValue(row, column.key))\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pecb-table__link\"\n (click)=\"$event.stopPropagation()\">\n {{ getCellValue(row, column.key) }}\n </a>\n </ng-container>\n\n <!-- Default Text -->\n <ng-container *ngIf=\"!column.cellTemplate && !hasDynamicComponent(column) && getColumnType(column) === 'text'\">\n {{ getCellValue(row, column.key) }}\n </ng-container>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination (outside table container) -->\n <div *ngIf=\"showPagination() && !loading() && data().length > 0\" [class]=\"paginationClasses\">\n <pecb-pagination\n [totalItems]=\"totalItems()\"\n [pageSize]=\"pageSize()\"\n [currentPage]=\"currentPage()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n [showPageSizeSelector]=\"showPageSizeSelector()\"\n [variant]=\"paginationVariant()\"\n size=\"md\"\n (pageChange)=\"onPageChange($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\">\n </pecb-pagination>\n </div>\n</div>\n", styles: [".pecb-table-wrapper{width:100%}.pecb-table-container{width:100%;background-color:#fff;border-radius:12px;overflow-x:auto;overflow-y:hidden;border:1px solid #ececec}.pecb-table-container--sticky{position:relative;overflow:auto}.pecb-table{width:100%;border-collapse:collapse;border-spacing:0;font-family:inherit;font-size:14px;color:#313131}.pecb-table--sm{font-size:12px}.pecb-table--sm .pecb-table__cell,.pecb-table--sm .pecb-table__cell--header{padding:8px 12px}.pecb-table--md{font-size:14px}.pecb-table--md .pecb-table__cell{padding:16px 18px}.pecb-table--md .pecb-table__cell--header{padding:12px 18px}.pecb-table--lg{font-size:16px}.pecb-table--lg .pecb-table__cell{padding:18px 20px}.pecb-table--lg .pecb-table__cell--header{padding:16px 20px}.pecb-table--default .pecb-table__row--data{border-bottom:1px solid #ececec}.pecb-table--default .pecb-table__row--data:last-child{border-bottom:none}.pecb-table--default .pecb-table__row--data:last-child .pecb-table__cell{border-bottom:none}.pecb-table--default .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--striped .pecb-table__row--data:nth-child(2n){background-color:#fafafa}.pecb-table--bordered .pecb-table__cell{border:1px solid #f7f5f5}.pecb-table--hoverable .pecb-table__row--data{transition:background-color .15s ease-in-out}.pecb-table--selectable .pecb-table__row--data{cursor:pointer}.pecb-table__head{background-color:#212427}.pecb-table__head--sticky{position:sticky;top:0;z-index:1020}.pecb-table__row--header{text-transform:uppercase;height:52px}.pecb-table__row--data{background-color:#fff;height:58px}.pecb-table__row--data:focus,.pecb-table__row--data:focus-visible{outline:none}.pecb-table__row--selected{background-color:#f4f4f4}.pecb-table__row--selected:hover{background-color:#f7f5f5}.pecb-table__row--empty{background-color:#fff}.pecb-table__cell{text-align:left;vertical-align:middle;white-space:nowrap;border-bottom:1px solid #ececec}.pecb-table__cell--header{font-weight:500;font-size:12px;color:#fff;letter-spacing:.02em;-webkit-user-select:none;user-select:none;border-bottom:none;background-color:#212427}.pecb-table__cell--sortable{cursor:pointer;transition:background-color .15s ease-in-out}.pecb-table__cell--sortable:hover{background-color:#373a3d}.pecb-table__cell--sortable:focus,.pecb-table__cell--sortable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px #fff}.pecb-table__cell--checkbox{width:48px;text-align:center}.pecb-table__cell--center{text-align:center}.pecb-table__cell--right{text-align:right}.pecb-table__cell--empty{text-align:center;padding:24px 0}.pecb-table__cell--truncate{overflow:hidden;text-overflow:ellipsis}.pecb-table__cell--wrap{white-space:normal;overflow-wrap:anywhere;word-break:break-word}.pecb-table__header-content{display:flex;align-items:center;gap:8px}.pecb-table__header-text{flex:1}.pecb-table__sort-icon{display:flex;align-items:center;justify-content:center;color:#ffffff80;transition:color .15s ease-in-out}.pecb-table__sort-icon svg{width:16px;height:16px}.pecb-table__sort-icon[data-sort=asc],.pecb-table__sort-icon[data-sort=desc]{color:#fff}.pecb-table__checkbox-wrapper{display:inline-flex;align-items:center;justify-content:center;position:relative;cursor:pointer}.pecb-table__checkbox{position:absolute;opacity:0;width:0;height:0}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:checked+.pecb-table__checkbox-custom:after{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom{background-color:#212427;border-color:#212427}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:before{display:block}.pecb-table__checkbox:indeterminate+.pecb-table__checkbox-custom:after{display:none}.pecb-table__checkbox:focus+.pecb-table__checkbox-custom{box-shadow:0 0 0 2px #21242733}.pecb-table__checkbox-custom{width:18px;height:18px;border:1.5px solid #c2c2c2;border-radius:6px;background-color:#fff;position:relative;transition:all .15s ease-in-out}.pecb-table__checkbox-custom:after{content:\"\";display:none;position:absolute;left:5px;top:2px;width:4px;height:8px;border:solid #ffffff;border-width:0 2px 2px 0;transform:rotate(45deg)}.pecb-table__checkbox-custom:before{content:\"\";display:none;position:absolute;left:3px;top:6px;width:8px;height:2px;background-color:#fff}.pecb-table__empty-content{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 0}.pecb-table__empty-message{color:#919191;font-size:14px}.pecb-table__loading{display:flex;align-items:center;justify-content:center;padding:48px;min-height:200px}.pecb-table__loading-spinner{display:flex;flex-direction:column;align-items:center;gap:12px;color:#616161;font-size:14px;font-family:inherit;font-weight:400}.pecb-table__spinner-icon{width:32px;height:32px;animation:spin 1s linear infinite;color:#212427}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pecb-table__pagination{padding:16px 0;margin-top:16px}.pecb-table__pagination--bordered{padding:16px 18px;background-color:#fff;border:1px solid #ececec;border-radius:6px}.pecb-table__user-cell{display:flex;align-items:center;gap:12px}.pecb-table__user-avatar{width:40px;height:40px;border-radius:9999px;object-fit:cover;flex-shrink:0}.pecb-table__user-info{display:flex;flex-direction:column;min-width:0}.pecb-table__user-name{font-weight:500;color:#313131;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__user-secondary{font-size:12px;color:#919191;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pecb-table__action-buttons{display:flex;align-items:center;gap:0;justify-content:center}.pecb-table__action-divider{display:block;width:1px;height:16px;background-color:#c2c2c2;flex-shrink:0}.pecb-table__action-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:transparent;color:#212427;cursor:pointer;border-radius:6px;transition:all .15s ease-in-out;padding:0}.pecb-table__action-btn:hover{background-color:#f7f5f5;color:#313131}.pecb-table__action-btn:focus{outline:none;box-shadow:0 0 0 2px #21242733}.pecb-table__action-btn:disabled{opacity:.5;cursor:not-allowed}.pecb-table__action-btn--danger:hover{background-color:#fdecea;color:#dc3545}.pecb-table__action-btn svg{width:18px;height:18px}.pecb-table__custom-icon{display:flex;align-items:center;justify-content:center}.pecb-table__custom-icon svg{width:18px;height:18px}.pecb-table__link{color:#212427;text-decoration:none;font-size:inherit}.pecb-table__link:hover{text-decoration:underline;color:#313131}.pecb-table__link:focus-visible{outline:2px solid #212427;outline-offset:2px;border-radius:2px}@media (min-width: 768px){.pecb-table--md,.pecb-table--lg{font-size:14px}.pecb-table--md .pecb-table__cell,.pecb-table--lg .pecb-table__cell{padding:12px 16px}.pecb-table--md .pecb-table__cell--header,.pecb-table--lg .pecb-table__cell--header{padding:8px 16px}.pecb-table__pagination{padding:12px 16px}}\n"] }]
4533
4533
  }], ctorParameters: () => [{ type: i1$1.DomSanitizer }, { type: i0.ChangeDetectorRef }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], hoverable: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverable", required: false }] }], showPagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPagination", required: false }] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: false }] }], currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: false }] }, { type: i0.Output, args: ["currentPageChange"] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }, { type: i0.Output, args: ["pageSizeChange"] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], showPageSizeSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPageSizeSelector", required: false }] }], paginationVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationVariant", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], selectedIndices: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndices", required: false }] }, { type: i0.Output, args: ["selectedIndicesChange"] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], wrapText: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapText", required: false }] }], sortColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortColumn", required: false }] }, { type: i0.Output, args: ["sortColumnChange"] }], sortDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDirection", required: false }] }, { type: i0.Output, args: ["sortDirectionChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], selectAllAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllAriaLabel", required: false }] }], selectRowAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectRowAriaLabel", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowAction: [{ type: i0.Output, args: ["rowAction"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], emptyTemplate: [{
4534
4534
  type: ContentChild,
4535
4535
  args: ['emptyTemplate']
@@ -10655,6 +10655,1819 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
10655
10655
  args: [{ selector: 'pecb-media', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div\n [class]=\"hostClasses()\"\n role=\"grid\"\n [attr.aria-label]=\"ariaLabel() ?? defaultAriaLabel()\"\n>\n @for (group of groups(); track group.label) {\n <div class=\"pecb-media__group\">\n @if (group.label) {\n <h3 class=\"pecb-media__group-title\">{{ group.label }}</h3>\n }\n\n <div class=\"pecb-media__grid\">\n @for (item of group.items; track item.name) {\n <button\n class=\"pecb-media__item\"\n type=\"button\"\n [style.width.px]=\"itemSize()\"\n [style.height.px]=\"itemSize()\"\n [attr.aria-label]=\"copyLabel() + ' ' + formatName(item.name)\"\n [attr.title]=\"formatName(item.name)\"\n (click)=\"copyToClipboard(item)\"\n >\n <span\n class=\"pecb-media__svg\"\n [innerHTML]=\"item.safeHtml\"\n ></span>\n\n @if (copiedItem() === item.name) {\n <span class=\"pecb-media__tooltip\">{{ copiedLabel() }}</span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".pecb-media{display:block;width:100%}.pecb-media__group{width:100%}.pecb-media__group+.pecb-media__group{margin-top:20px}.pecb-media__group-title{font-family:Plus Jakarta Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:16px;font-weight:700;line-height:1.4;color:#313131;margin:0 0 16px}.pecb-media__grid{display:flex;flex-wrap:wrap;gap:12px;align-items:center}.pecb-media__item{position:relative;display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;border-radius:6px;cursor:pointer;transition-property:background-color,transform;transition-duration:.25s ease-in-out;transition-timing-function:ease-in-out}.pecb-media__item:hover{background-color:#f4f4f4;transform:scale(1.1)}.pecb-media__item:active{transform:scale(.95)}.pecb-media__item:focus-visible{outline:2px solid #007bff;outline-offset:2px}.pecb-media__svg{display:inline-flex;align-items:center;justify-content:center;width:100%;height:100%}.pecb-media__svg svg{display:block;width:100%;height:100%}.pecb-media__tooltip{position:absolute;bottom:calc(100% + 4px);left:50%;transform:translate(-50%);padding:2px 8px;background-color:#212427;color:#fff;font-family:Plus Jakarta Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:12px;font-weight:500;line-height:20px;border-radius:6px;white-space:nowrap;pointer-events:none;animation:pecb-media-fade-in .15s ease-out;z-index:1070}.pecb-media--sm .pecb-media__grid{gap:8px}.pecb-media--lg .pecb-media__grid{gap:16px}.pecb-media--flag .pecb-media__item{overflow:hidden}@keyframes pecb-media-fade-in{0%{opacity:0;transform:translate(-50%) translateY(4px)}to{opacity:1;transform:translate(-50%) translateY(0)}}\n"] }]
10656
10656
  }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], socialStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "socialStyle", required: false }] }], flagShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "flagShape", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], defaultAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultAriaLabel", required: false }] }], copiedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "copiedLabel", required: false }] }], copyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyLabel", required: false }] }] } });
10657
10657
 
10658
+ const DEFAULT_VIDEO_PLAYER_LABELS = {
10659
+ play: 'Play',
10660
+ pause: 'Pause',
10661
+ replay: 'Replay',
10662
+ skipBack: 'Rewind {n} seconds',
10663
+ mute: 'Mute',
10664
+ unmute: 'Unmute',
10665
+ volume: 'Volume',
10666
+ captionsOn: 'Turn captions on',
10667
+ captionsOff: 'Turn captions off',
10668
+ settings: 'Settings',
10669
+ playbackSpeed: 'Playback speed',
10670
+ normalSpeed: 'Normal',
10671
+ enterFullscreen: 'Enter fullscreen',
10672
+ exitFullscreen: 'Exit fullscreen',
10673
+ seek: 'Seek',
10674
+ player: 'Video player',
10675
+ error: 'This video could not be loaded.',
10676
+ };
10677
+ /** Default playback-rate menu entries. */
10678
+ const DEFAULT_VIDEO_PLAYER_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
10679
+ /**
10680
+ * Formats seconds as `m:ss`, or `h:mm:ss` once the value reaches one hour.
10681
+ * Non-finite / negative input renders as `0:00`.
10682
+ */
10683
+ function formatVideoTime(seconds) {
10684
+ if (!Number.isFinite(seconds) || seconds < 0) {
10685
+ return '0:00';
10686
+ }
10687
+ const total = Math.floor(seconds);
10688
+ const h = Math.floor(total / 3600);
10689
+ const m = Math.floor((total % 3600) / 60);
10690
+ const s = total % 60;
10691
+ const mm = h > 0 ? String(m).padStart(2, '0') : String(m);
10692
+ const ss = String(s).padStart(2, '0');
10693
+ return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
10694
+ }
10695
+ // Internal tuning constants.
10696
+ const IDLE_HIDE_DELAY_MS = 2600;
10697
+ const LEAVE_HIDE_DELAY_MS = 700;
10698
+ const SEEK_SYNC_TOLERANCE_S = 0.5;
10699
+ // ─── Component ────────────────────────────────────────────────────────────────
10700
+ /**
10701
+ * PECB course video player.
10702
+ *
10703
+ * A fully custom-skinned, accessible HTML5 `<video>` wrapper that reproduces
10704
+ * the PECB "Course Player" design: a rounded dark 16:9 stage, poster, brand
10705
+ * watermark + section label, a blurred centre play button and a bottom
10706
+ * control bar (progress, play/pause, rewind, volume, time, captions, speed
10707
+ * settings, fullscreen).
10708
+ *
10709
+ * The player is self-contained (no icon-registry / service dependencies) so it
10710
+ * can be dropped into any Angular 17+ app. It is responsive (container
10711
+ * queries), keyboard operable, touch friendly and honours
10712
+ * `prefers-reduced-motion`.
10713
+ *
10714
+ * @example Minimal
10715
+ * ```html
10716
+ * <pecb-video-player
10717
+ * src="https://cdn.example.com/lesson.mp4"
10718
+ * poster="https://cdn.example.com/lesson.jpg"
10719
+ * label="SECTION 2">
10720
+ * </pecb-video-player>
10721
+ * ```
10722
+ *
10723
+ * @example Two-way bound & driven from outside (e.g. a transcript panel)
10724
+ * ```html
10725
+ * <pecb-video-player
10726
+ * [src]="sources"
10727
+ * [tracks]="captions"
10728
+ * [(currentTime)]="at"
10729
+ * [(playing)]="playing"
10730
+ * (playbackEnded)="onLessonFinished()">
10731
+ * </pecb-video-player>
10732
+ * ```
10733
+ *
10734
+ * Theming — override the CSS custom properties on the host:
10735
+ * ```css
10736
+ * pecb-video-player { --pecb-video-radius: 0; --pecb-video-accent: #a11e29; }
10737
+ * ```
10738
+ */
10739
+ class VideoPlayerComponent {
10740
+ constructor() {
10741
+ this.platformId = inject(PLATFORM_ID);
10742
+ this.zone = inject(NgZone);
10743
+ this.destroyRef = inject(DestroyRef);
10744
+ this.isBrowser = isPlatformBrowser(this.platformId);
10745
+ /** Unique id used to wire ARIA relationships inside the template. */
10746
+ this.uid = generateUniqueId('pecb-video');
10747
+ // ── Element refs ────────────────────────────────────────────────────────────
10748
+ this.rootRef = viewChild('root', ...(ngDevMode ? [{ debugName: "rootRef" }] : []));
10749
+ this.videoRef = viewChild('video', ...(ngDevMode ? [{ debugName: "videoRef" }] : []));
10750
+ this.progressRef = viewChild('progress', ...(ngDevMode ? [{ debugName: "progressRef" }] : []));
10751
+ this.settingsMenuRef = viewChild('settingsMenu', ...(ngDevMode ? [{ debugName: "settingsMenuRef" }] : []));
10752
+ // ── Media inputs ────────────────────────────────────────────────────────────
10753
+ /** Media URL, or a list of `<source>` candidates. */
10754
+ this.src = input(undefined, ...(ngDevMode ? [{ debugName: "src" }] : []));
10755
+ /** Poster image shown before playback starts. */
10756
+ this.poster = input(undefined, ...(ngDevMode ? [{ debugName: "poster" }] : []));
10757
+ /** Text tracks (captions / subtitles). The CC button appears when non-empty. */
10758
+ this.tracks = input([], ...(ngDevMode ? [{ debugName: "tracks" }] : []));
10759
+ /** Accessible title of the media, announced as part of the region label. */
10760
+ this.title = input(undefined, ...(ngDevMode ? [{ debugName: "title" }] : []));
10761
+ /** `crossorigin` attribute — required for captions hosted on another origin. */
10762
+ this.crossOrigin = input(undefined, ...(ngDevMode ? [{ debugName: "crossOrigin" }] : []));
10763
+ /** @default 'metadata' */
10764
+ this.preload = input('metadata', ...(ngDevMode ? [{ debugName: "preload" }] : []));
10765
+ /** Attempt to start playback automatically (subject to browser policy). */
10766
+ this.autoplay = input(false, ...(ngDevMode ? [{ debugName: "autoplay", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10767
+ /** Restart from the beginning when the media ends. */
10768
+ this.loop = input(false, ...(ngDevMode ? [{ debugName: "loop", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10769
+ /** Play inline on iOS instead of forcing the native fullscreen player. @default true */
10770
+ this.playsInline = input(true, ...(ngDevMode ? [{ debugName: "playsInline", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10771
+ /** Initial playhead position in seconds (applied once metadata is loaded). */
10772
+ this.startTime = input(0, ...(ngDevMode ? [{ debugName: "startTime", transform: numberAttribute }] : [{ transform: numberAttribute }]));
10773
+ // ── Appearance inputs ───────────────────────────────────────────────────────
10774
+ /** Brand watermark in the bottom-left corner. Empty string hides it. @default 'PECB' */
10775
+ this.watermark = input('PECB', ...(ngDevMode ? [{ debugName: "watermark" }] : []));
10776
+ /** Large label in the bottom-right corner, e.g. `SECTION 2`. */
10777
+ this.label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
10778
+ /** CSS `aspect-ratio` of the stage. @default '16 / 9' */
10779
+ this.aspectRatio = input('16 / 9', ...(ngDevMode ? [{ debugName: "aspectRatio" }] : []));
10780
+ /** Show the large centre play/pause button. @default true */
10781
+ this.showCenterButton = input(true, ...(ngDevMode ? [{ debugName: "showCenterButton", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10782
+ /** Show the rewind button. @default true */
10783
+ this.showSkipBack = input(true, ...(ngDevMode ? [{ debugName: "showSkipBack", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10784
+ /** Show the mute button + volume slider. @default true */
10785
+ this.showVolume = input(true, ...(ngDevMode ? [{ debugName: "showVolume", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10786
+ /** Show the elapsed / total time. @default true */
10787
+ this.showTime = input(true, ...(ngDevMode ? [{ debugName: "showTime", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10788
+ /** Force-show/hide the captions button. `undefined` = show only when `tracks` is non-empty. */
10789
+ this.showCaptions = input(undefined, ...(ngDevMode ? [{ debugName: "showCaptions" }] : []));
10790
+ /** Show the settings (playback speed) button. @default true */
10791
+ this.showSettings = input(true, ...(ngDevMode ? [{ debugName: "showSettings", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10792
+ /** Show the fullscreen button. @default true */
10793
+ this.showFullscreen = input(true, ...(ngDevMode ? [{ debugName: "showFullscreen", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10794
+ /** Seconds the rewind button / arrow keys jump. @default 10 */
10795
+ this.seekStep = input(10, ...(ngDevMode ? [{ debugName: "seekStep", transform: numberAttribute }] : [{ transform: numberAttribute }]));
10796
+ /** Playback-rate options listed in the settings menu. */
10797
+ this.playbackRates = input(DEFAULT_VIDEO_PLAYER_RATES, ...(ngDevMode ? [{ debugName: "playbackRates" }] : []));
10798
+ /** Partial override of the user-visible strings (i18n). */
10799
+ this.labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : []));
10800
+ // ── Two-way state (models) ──────────────────────────────────────────────────
10801
+ /** Whether the media is playing. Set it to start/stop playback. */
10802
+ this.playing = model(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
10803
+ /** Playhead position in seconds. Set it to seek. */
10804
+ this.currentTime = model(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
10805
+ /** Volume 0–1. */
10806
+ this.volume = model(1, ...(ngDevMode ? [{ debugName: "volume" }] : []));
10807
+ /** Muted state. */
10808
+ this.muted = model(false, ...(ngDevMode ? [{ debugName: "muted" }] : []));
10809
+ /** Playback rate. */
10810
+ this.playbackRate = model(1, ...(ngDevMode ? [{ debugName: "playbackRate" }] : []));
10811
+ /** Whether the default text track is displayed. */
10812
+ this.captionsEnabled = model(false, ...(ngDevMode ? [{ debugName: "captionsEnabled" }] : []));
10813
+ // ── Outputs ─────────────────────────────────────────────────────────────────
10814
+ /** Fires on every `timeupdate` (~4×/s while playing). */
10815
+ this.timeUpdate = output();
10816
+ /** Duration is known. */
10817
+ this.loadedMetadata = output();
10818
+ /** A seek finished. */
10819
+ this.seekEnd = output();
10820
+ /** Playback reached the end. */
10821
+ this.playbackEnded = output();
10822
+ /** Player entered / left fullscreen. */
10823
+ this.fullscreenChange = output();
10824
+ /** Media failed to load or playback was rejected. */
10825
+ this.playerError = output();
10826
+ // ── Internal state ──────────────────────────────────────────────────────────
10827
+ this.duration = signal(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
10828
+ this.bufferedEnd = signal(0, ...(ngDevMode ? [{ debugName: "bufferedEnd" }] : []));
10829
+ this.hasEnded = signal(false, ...(ngDevMode ? [{ debugName: "hasEnded" }] : []));
10830
+ this.isBuffering = signal(false, ...(ngDevMode ? [{ debugName: "isBuffering" }] : []));
10831
+ this.hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
10832
+ this.isFullscreen = signal(false, ...(ngDevMode ? [{ debugName: "isFullscreen" }] : []));
10833
+ this.isSeeking = signal(false, ...(ngDevMode ? [{ debugName: "isSeeking" }] : []));
10834
+ this.settingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "settingsOpen" }] : []));
10835
+ this.controlsVisible = signal(true, ...(ngDevMode ? [{ debugName: "controlsVisible" }] : []));
10836
+ this.hasStarted = signal(false, ...(ngDevMode ? [{ debugName: "hasStarted" }] : []));
10837
+ this.hideTimer = null;
10838
+ this.lastPointerType = 'mouse';
10839
+ this.startTimeApplied = false;
10840
+ this.initialLoadDone = false;
10841
+ this.pendingPlay = null;
10842
+ // ── Computed ────────────────────────────────────────────────────────────────
10843
+ this.text = computed(() => ({
10844
+ ...DEFAULT_VIDEO_PLAYER_LABELS,
10845
+ ...this.labels(),
10846
+ }), ...(ngDevMode ? [{ debugName: "text" }] : []));
10847
+ this.skipBackLabel = computed(() => this.text().skipBack.replace('{n}', String(this.seekStep())), ...(ngDevMode ? [{ debugName: "skipBackLabel" }] : []));
10848
+ this.sourceList = computed(() => {
10849
+ const s = this.src();
10850
+ if (!s)
10851
+ return [];
10852
+ return typeof s === 'string' ? [{ src: s }] : s;
10853
+ }, ...(ngDevMode ? [{ debugName: "sourceList" }] : []));
10854
+ /** Single-string src is bound directly so the browser can start fetching ASAP. */
10855
+ this.singleSrc = computed(() => {
10856
+ const s = this.src();
10857
+ return typeof s === 'string' ? s : undefined;
10858
+ }, ...(ngDevMode ? [{ debugName: "singleSrc" }] : []));
10859
+ this.progressPct = computed(() => {
10860
+ const d = this.duration();
10861
+ return d > 0 ? Math.min(100, Math.max(0, (this.currentTime() / d) * 100)) : 0;
10862
+ }, ...(ngDevMode ? [{ debugName: "progressPct" }] : []));
10863
+ this.bufferedPct = computed(() => {
10864
+ const d = this.duration();
10865
+ return d > 0 ? Math.min(100, Math.max(0, (this.bufferedEnd() / d) * 100)) : 0;
10866
+ }, ...(ngDevMode ? [{ debugName: "bufferedPct" }] : []));
10867
+ this.currentTimeLabel = computed(() => formatVideoTime(this.currentTime()), ...(ngDevMode ? [{ debugName: "currentTimeLabel" }] : []));
10868
+ this.durationLabel = computed(() => formatVideoTime(this.duration()), ...(ngDevMode ? [{ debugName: "durationLabel" }] : []));
10869
+ this.captionsButtonVisible = computed(() => this.showCaptions() ?? this.tracks().length > 0, ...(ngDevMode ? [{ debugName: "captionsButtonVisible" }] : []));
10870
+ /** Volume as an integer percentage (0 while muted) — drives the slider. */
10871
+ this.volumePct = computed(() => (this.muted() ? 0 : Math.round(this.volume() * 100)), ...(ngDevMode ? [{ debugName: "volumePct" }] : []));
10872
+ this.volumeLevel = computed(() => {
10873
+ if (this.muted() || this.volume() === 0)
10874
+ return 'muted';
10875
+ return this.volume() < 0.5 ? 'low' : 'high';
10876
+ }, ...(ngDevMode ? [{ debugName: "volumeLevel" }] : []));
10877
+ this.regionLabel = computed(() => {
10878
+ const t = this.title();
10879
+ return t ? `${this.text().player}: ${t}` : this.text().player;
10880
+ }, ...(ngDevMode ? [{ debugName: "regionLabel" }] : []));
10881
+ /** Controls are hidden only while playing and idle. */
10882
+ this.controlsHidden = computed(() => this.playing() &&
10883
+ !this.controlsVisible() &&
10884
+ !this.settingsOpen() &&
10885
+ !this.isSeeking() &&
10886
+ !this.hasEnded(), ...(ngDevMode ? [{ debugName: "controlsHidden" }] : []));
10887
+ this.hostClasses = computed(() => [
10888
+ 'pecb-video-player',
10889
+ this.playing() ? 'pecb-video-player--playing' : 'pecb-video-player--paused',
10890
+ this.controlsHidden() ? 'pecb-video-player--idle' : '',
10891
+ this.isFullscreen() ? 'pecb-video-player--fullscreen' : '',
10892
+ this.hasEnded() ? 'pecb-video-player--ended' : '',
10893
+ this.hasError() ? 'pecb-video-player--error' : '',
10894
+ this.isSeeking() ? 'pecb-video-player--seeking' : '',
10895
+ ]
10896
+ .filter(Boolean)
10897
+ .join(' '), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
10898
+ // ── model → <video> synchronisation ───────────────────────────────────────
10899
+ effect(() => {
10900
+ const wantPlaying = this.playing();
10901
+ const video = this.videoRef()?.nativeElement;
10902
+ if (!video)
10903
+ return;
10904
+ untracked(() => {
10905
+ if (wantPlaying && video.paused) {
10906
+ this.safePlay(video);
10907
+ }
10908
+ else if (!wantPlaying && !video.paused) {
10909
+ video.pause();
10910
+ }
10911
+ });
10912
+ });
10913
+ effect(() => {
10914
+ const t = this.currentTime();
10915
+ const video = this.videoRef()?.nativeElement;
10916
+ if (!video || !Number.isFinite(t))
10917
+ return;
10918
+ untracked(() => {
10919
+ if (Math.abs(video.currentTime - t) > SEEK_SYNC_TOLERANCE_S) {
10920
+ video.currentTime = t;
10921
+ }
10922
+ });
10923
+ });
10924
+ effect(() => {
10925
+ const v = Math.min(1, Math.max(0, this.volume()));
10926
+ const m = this.muted();
10927
+ const video = this.videoRef()?.nativeElement;
10928
+ if (!video)
10929
+ return;
10930
+ untracked(() => {
10931
+ if (video.volume !== v)
10932
+ video.volume = v;
10933
+ if (video.muted !== m)
10934
+ video.muted = m;
10935
+ });
10936
+ });
10937
+ effect(() => {
10938
+ const rate = this.playbackRate();
10939
+ const video = this.videoRef()?.nativeElement;
10940
+ if (!video || !Number.isFinite(rate) || rate <= 0)
10941
+ return;
10942
+ untracked(() => {
10943
+ if (video.playbackRate !== rate)
10944
+ video.playbackRate = rate;
10945
+ });
10946
+ });
10947
+ effect(() => {
10948
+ const enabled = this.captionsEnabled();
10949
+ // Re-run when tracks change (they are re-rendered by the template).
10950
+ this.tracks();
10951
+ const video = this.videoRef()?.nativeElement;
10952
+ if (!video)
10953
+ return;
10954
+ untracked(() => this.applyCaptionMode(video, enabled));
10955
+ });
10956
+ // Reload the element when the source set changes after first render.
10957
+ effect(() => {
10958
+ this.sourceList();
10959
+ const video = this.videoRef()?.nativeElement;
10960
+ if (!video)
10961
+ return;
10962
+ untracked(() => {
10963
+ if (!this.initialLoadDone) {
10964
+ this.initialLoadDone = true;
10965
+ return;
10966
+ }
10967
+ this.resetForNewSource(video);
10968
+ });
10969
+ });
10970
+ if (this.isBrowser) {
10971
+ afterNextRender(() => this.attachIdleListeners());
10972
+ this.destroyRef.onDestroy(() => this.clearHideTimer());
10973
+ }
10974
+ }
10975
+ // ── Public API ──────────────────────────────────────────────────────────────
10976
+ /** Start playback. */
10977
+ play() {
10978
+ this.playing.set(true);
10979
+ }
10980
+ /** Pause playback. */
10981
+ pause() {
10982
+ this.playing.set(false);
10983
+ }
10984
+ /** Toggle play / pause (replays from the start after the media ended). */
10985
+ togglePlay() {
10986
+ if (this.hasError())
10987
+ return;
10988
+ if (this.hasEnded()) {
10989
+ this.seek(0);
10990
+ this.playing.set(true);
10991
+ return;
10992
+ }
10993
+ this.playing.update((p) => !p);
10994
+ }
10995
+ /** Seek to an absolute position (seconds, clamped to the duration). */
10996
+ seek(seconds) {
10997
+ const d = this.duration();
10998
+ const target = Math.max(0, d > 0 ? Math.min(d, seconds) : seconds);
10999
+ const video = this.videoRef()?.nativeElement;
11000
+ if (video) {
11001
+ video.currentTime = target;
11002
+ }
11003
+ this.currentTime.set(target);
11004
+ }
11005
+ /** Seek relative to the current position. */
11006
+ seekBy(deltaSeconds) {
11007
+ this.seek(this.currentTime() + deltaSeconds);
11008
+ }
11009
+ /** Rewind by `seekStep` seconds. */
11010
+ skipBack() {
11011
+ this.seekBy(-this.seekStep());
11012
+ }
11013
+ /** Toggle mute. Unmuting a zero volume restores it to 50 %. */
11014
+ toggleMute() {
11015
+ const nextMuted = !this.muted();
11016
+ if (!nextMuted && this.volume() === 0) {
11017
+ this.volume.set(0.5);
11018
+ }
11019
+ this.muted.set(nextMuted);
11020
+ }
11021
+ /** Set volume (0–1). A non-zero value also unmutes. */
11022
+ setVolume(value) {
11023
+ const v = Math.min(1, Math.max(0, value));
11024
+ this.volume.set(v);
11025
+ if (v > 0 && this.muted()) {
11026
+ this.muted.set(false);
11027
+ }
11028
+ }
11029
+ /** Set the playback rate and close the settings menu. */
11030
+ setPlaybackRate(rate) {
11031
+ this.playbackRate.set(rate);
11032
+ this.closeSettings(true);
11033
+ }
11034
+ /** Toggle captions on the default text track. */
11035
+ toggleCaptions() {
11036
+ this.captionsEnabled.update((c) => !c);
11037
+ }
11038
+ /** Enter / exit fullscreen for the whole player (falls back to the native iOS player). */
11039
+ async toggleFullscreen() {
11040
+ if (!this.isBrowser)
11041
+ return;
11042
+ const root = this.rootRef()?.nativeElement;
11043
+ const video = this.videoRef()?.nativeElement;
11044
+ if (!root || !video)
11045
+ return;
11046
+ try {
11047
+ if (document.fullscreenElement) {
11048
+ await document.exitFullscreen();
11049
+ }
11050
+ else if (root.requestFullscreen) {
11051
+ await root.requestFullscreen();
11052
+ }
11053
+ else {
11054
+ // iOS Safari: no element fullscreen — use the native video fullscreen.
11055
+ const legacy = video;
11056
+ legacy.webkitEnterFullscreen?.();
11057
+ }
11058
+ }
11059
+ catch {
11060
+ // Fullscreen can be denied (iframe sandbox, user-gesture rules) — ignore.
11061
+ }
11062
+ }
11063
+ /** Open / close the settings (playback speed) menu. */
11064
+ toggleSettings() {
11065
+ if (this.settingsOpen()) {
11066
+ this.closeSettings(true);
11067
+ return;
11068
+ }
11069
+ this.settingsOpen.set(true);
11070
+ this.revealControls();
11071
+ // Focus the checked item once the menu is rendered.
11072
+ queueMicrotask(() => {
11073
+ const menu = this.settingsMenuRef()?.nativeElement;
11074
+ const target = menu?.querySelector('[aria-checked="true"]') ??
11075
+ menu?.querySelector('[role="menuitemradio"]');
11076
+ target?.focus();
11077
+ });
11078
+ }
11079
+ /** Close the settings menu; optionally move focus back to its trigger. */
11080
+ closeSettings(restoreFocus = false) {
11081
+ if (!this.settingsOpen())
11082
+ return;
11083
+ this.settingsOpen.set(false);
11084
+ if (restoreFocus) {
11085
+ this.rootRef()
11086
+ ?.nativeElement.querySelector(`#${this.uid}-settings-btn`)
11087
+ ?.focus();
11088
+ }
11089
+ this.scheduleHide();
11090
+ }
11091
+ // ── <video> event handlers ──────────────────────────────────────────────────
11092
+ onLoadedMetadata(video) {
11093
+ this.duration.set(Number.isFinite(video.duration) ? video.duration : 0);
11094
+ this.hasError.set(false);
11095
+ if (!this.startTimeApplied) {
11096
+ this.startTimeApplied = true;
11097
+ const start = this.startTime() > 0 ? this.startTime() : this.currentTime();
11098
+ if (start > 0) {
11099
+ video.currentTime = Math.min(start, video.duration || start);
11100
+ }
11101
+ }
11102
+ // Re-apply model values in case the effects ran before the element existed.
11103
+ video.volume = Math.min(1, Math.max(0, this.volume()));
11104
+ video.muted = this.muted();
11105
+ video.playbackRate = this.playbackRate();
11106
+ this.applyCaptionMode(video, this.captionsEnabled());
11107
+ this.loadedMetadata.emit({ currentTime: video.currentTime, duration: video.duration });
11108
+ }
11109
+ onDurationChange(video) {
11110
+ this.duration.set(Number.isFinite(video.duration) ? video.duration : 0);
11111
+ }
11112
+ onTimeUpdate(video) {
11113
+ if (!this.isSeeking()) {
11114
+ this.currentTime.set(video.currentTime);
11115
+ }
11116
+ this.timeUpdate.emit({ currentTime: video.currentTime, duration: video.duration });
11117
+ }
11118
+ onProgress(video) {
11119
+ const ranges = video.buffered;
11120
+ if (!ranges || ranges.length === 0)
11121
+ return;
11122
+ // Prefer the range containing the playhead, else the last one.
11123
+ let end = ranges.end(ranges.length - 1);
11124
+ for (let i = 0; i < ranges.length; i++) {
11125
+ if (ranges.start(i) <= video.currentTime && video.currentTime <= ranges.end(i)) {
11126
+ end = ranges.end(i);
11127
+ break;
11128
+ }
11129
+ }
11130
+ this.bufferedEnd.set(end);
11131
+ }
11132
+ onPlay() {
11133
+ this.hasEnded.set(false);
11134
+ this.hasStarted.set(true);
11135
+ this.playing.set(true);
11136
+ this.scheduleHide();
11137
+ }
11138
+ onPause() {
11139
+ this.playing.set(false);
11140
+ this.isBuffering.set(false);
11141
+ this.revealControls(false);
11142
+ }
11143
+ onEnded() {
11144
+ this.hasEnded.set(true);
11145
+ this.playing.set(false);
11146
+ this.revealControls(false);
11147
+ this.playbackEnded.emit();
11148
+ }
11149
+ onWaiting() {
11150
+ this.isBuffering.set(true);
11151
+ }
11152
+ onPlaying() {
11153
+ this.isBuffering.set(false);
11154
+ }
11155
+ onSeeked(video) {
11156
+ this.isBuffering.set(false);
11157
+ if (this.hasEnded() && video.currentTime < video.duration) {
11158
+ this.hasEnded.set(false);
11159
+ }
11160
+ this.seekEnd.emit({ currentTime: video.currentTime, duration: video.duration });
11161
+ }
11162
+ onVolumeChange(video) {
11163
+ this.volume.set(video.volume);
11164
+ this.muted.set(video.muted);
11165
+ }
11166
+ onRateChange(video) {
11167
+ this.playbackRate.set(video.playbackRate);
11168
+ }
11169
+ onMediaError(video) {
11170
+ // Ignore the spurious error fired when the element is reset with no source.
11171
+ if (!video.currentSrc && this.sourceList().length === 0)
11172
+ return;
11173
+ this.hasError.set(true);
11174
+ this.isBuffering.set(false);
11175
+ this.playing.set(false);
11176
+ this.playerError.emit({
11177
+ code: 'media',
11178
+ message: video.error?.message || 'Media error',
11179
+ originalError: video.error ?? undefined,
11180
+ });
11181
+ }
11182
+ // ── Stage interaction ───────────────────────────────────────────────────────
11183
+ onStagePointerDown(event) {
11184
+ this.lastPointerType = event.pointerType || 'mouse';
11185
+ }
11186
+ /** Click on the video surface: toggles play on mouse; on touch, first reveals the controls. */
11187
+ onStageClick() {
11188
+ if (this.settingsOpen()) {
11189
+ this.closeSettings();
11190
+ return;
11191
+ }
11192
+ if (this.lastPointerType === 'touch' && this.controlsHidden()) {
11193
+ this.revealControls();
11194
+ return;
11195
+ }
11196
+ this.togglePlay();
11197
+ }
11198
+ onStageDoubleClick() {
11199
+ if (this.lastPointerType !== 'touch' && this.showFullscreen()) {
11200
+ void this.toggleFullscreen();
11201
+ }
11202
+ }
11203
+ /** Keyboard shortcuts on the player region. */
11204
+ onRootKeydown(event) {
11205
+ const target = event.target;
11206
+ const root = this.rootRef()?.nativeElement;
11207
+ if (event.key === 'Escape' && this.settingsOpen()) {
11208
+ event.preventDefault();
11209
+ this.closeSettings(true);
11210
+ return;
11211
+ }
11212
+ // Let native controls (buttons, sliders, menu) handle their own keys.
11213
+ const onInteractive = !!target &&
11214
+ target !== root &&
11215
+ (target.matches('button, input, [role="slider"], [role="menuitemradio"]') ||
11216
+ !!target.closest('[role="menu"]'));
11217
+ const nativeKey = event.key === ' ' ||
11218
+ event.key === 'Enter' ||
11219
+ event.key.startsWith('Arrow') ||
11220
+ event.key === 'Home' ||
11221
+ event.key === 'End';
11222
+ if (onInteractive && nativeKey) {
11223
+ this.revealControls();
11224
+ return;
11225
+ }
11226
+ let handled = true;
11227
+ switch (event.key) {
11228
+ case ' ':
11229
+ case 'k':
11230
+ case 'K':
11231
+ this.togglePlay();
11232
+ break;
11233
+ case 'ArrowLeft':
11234
+ case 'j':
11235
+ case 'J':
11236
+ this.seekBy(-this.seekStep());
11237
+ break;
11238
+ case 'ArrowRight':
11239
+ case 'l':
11240
+ case 'L':
11241
+ this.seekBy(this.seekStep());
11242
+ break;
11243
+ case 'ArrowUp':
11244
+ if (this.showVolume())
11245
+ this.setVolume(this.volume() + 0.1);
11246
+ break;
11247
+ case 'ArrowDown':
11248
+ if (this.showVolume())
11249
+ this.setVolume(this.volume() - 0.1);
11250
+ break;
11251
+ case 'Home':
11252
+ this.seek(0);
11253
+ break;
11254
+ case 'End':
11255
+ this.seek(this.duration());
11256
+ break;
11257
+ case 'm':
11258
+ case 'M':
11259
+ if (this.showVolume())
11260
+ this.toggleMute();
11261
+ break;
11262
+ case 'f':
11263
+ case 'F':
11264
+ if (this.showFullscreen())
11265
+ void this.toggleFullscreen();
11266
+ break;
11267
+ case 'c':
11268
+ case 'C':
11269
+ if (this.captionsButtonVisible())
11270
+ this.toggleCaptions();
11271
+ break;
11272
+ default:
11273
+ handled = false;
11274
+ }
11275
+ if (handled) {
11276
+ event.preventDefault();
11277
+ this.revealControls();
11278
+ }
11279
+ }
11280
+ // ── Progress / scrubbing ────────────────────────────────────────────────────
11281
+ onProgressPointerDown(event) {
11282
+ if (event.pointerType === 'mouse' && event.button !== 0)
11283
+ return;
11284
+ const bar = this.progressRef()?.nativeElement;
11285
+ if (!bar || this.duration() <= 0)
11286
+ return;
11287
+ event.preventDefault();
11288
+ bar.setPointerCapture(event.pointerId);
11289
+ this.isSeeking.set(true);
11290
+ this.seekToPointer(event, bar);
11291
+ }
11292
+ onProgressPointerMove(event) {
11293
+ if (!this.isSeeking())
11294
+ return;
11295
+ this.seekToPointer(event, this.progressRef()?.nativeElement);
11296
+ }
11297
+ onProgressPointerUp(event) {
11298
+ if (!this.isSeeking())
11299
+ return;
11300
+ const bar = this.progressRef()?.nativeElement;
11301
+ if (bar?.hasPointerCapture(event.pointerId)) {
11302
+ bar.releasePointerCapture(event.pointerId);
11303
+ }
11304
+ this.seekToPointer(event, bar);
11305
+ this.isSeeking.set(false);
11306
+ this.scheduleHide();
11307
+ }
11308
+ onProgressKeydown(event) {
11309
+ const d = this.duration();
11310
+ if (d <= 0)
11311
+ return;
11312
+ let handled = true;
11313
+ switch (event.key) {
11314
+ case 'ArrowLeft':
11315
+ case 'ArrowDown':
11316
+ this.seekBy(-this.seekStep());
11317
+ break;
11318
+ case 'ArrowRight':
11319
+ case 'ArrowUp':
11320
+ this.seekBy(this.seekStep());
11321
+ break;
11322
+ case 'PageDown':
11323
+ this.seekBy(-d * 0.1);
11324
+ break;
11325
+ case 'PageUp':
11326
+ this.seekBy(d * 0.1);
11327
+ break;
11328
+ case 'Home':
11329
+ this.seek(0);
11330
+ break;
11331
+ case 'End':
11332
+ this.seek(d);
11333
+ break;
11334
+ default:
11335
+ handled = false;
11336
+ }
11337
+ if (handled) {
11338
+ event.preventDefault();
11339
+ event.stopPropagation();
11340
+ this.revealControls();
11341
+ }
11342
+ }
11343
+ seekToPointer(event, bar) {
11344
+ if (!bar)
11345
+ return;
11346
+ const rect = bar.getBoundingClientRect();
11347
+ if (rect.width <= 0)
11348
+ return;
11349
+ const fraction = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
11350
+ this.seek(fraction * this.duration());
11351
+ }
11352
+ // ── Volume slider ───────────────────────────────────────────────────────────
11353
+ onVolumeInput(event) {
11354
+ const value = Number(event.target.value);
11355
+ this.setVolume(value / 100);
11356
+ }
11357
+ // ── Settings menu ───────────────────────────────────────────────────────────
11358
+ onSettingsMenuKeydown(event) {
11359
+ const menu = this.settingsMenuRef()?.nativeElement;
11360
+ if (!menu)
11361
+ return;
11362
+ const items = Array.from(menu.querySelectorAll('[role="menuitemradio"]'));
11363
+ if (items.length === 0)
11364
+ return;
11365
+ const index = items.indexOf(document.activeElement);
11366
+ let next;
11367
+ switch (event.key) {
11368
+ case 'ArrowDown':
11369
+ next = (index + 1) % items.length;
11370
+ break;
11371
+ case 'ArrowUp':
11372
+ next = (index - 1 + items.length) % items.length;
11373
+ break;
11374
+ case 'Home':
11375
+ next = 0;
11376
+ break;
11377
+ case 'End':
11378
+ next = items.length - 1;
11379
+ break;
11380
+ case 'Tab':
11381
+ this.closeSettings();
11382
+ return;
11383
+ default:
11384
+ return;
11385
+ }
11386
+ event.preventDefault();
11387
+ event.stopPropagation();
11388
+ items[next]?.focus();
11389
+ }
11390
+ onRootFocusOut(event) {
11391
+ const root = this.rootRef()?.nativeElement;
11392
+ const next = event.relatedTarget;
11393
+ if (this.settingsOpen() && root && (!next || !root.contains(next))) {
11394
+ this.closeSettings();
11395
+ }
11396
+ }
11397
+ onDocumentFullscreenChange() {
11398
+ if (!this.isBrowser)
11399
+ return;
11400
+ const root = this.rootRef()?.nativeElement;
11401
+ const active = !!root && document.fullscreenElement === root;
11402
+ if (active !== this.isFullscreen()) {
11403
+ this.isFullscreen.set(active);
11404
+ this.fullscreenChange.emit(active);
11405
+ }
11406
+ }
11407
+ onDocumentPointerDown(event) {
11408
+ if (!this.settingsOpen())
11409
+ return;
11410
+ const root = this.rootRef()?.nativeElement;
11411
+ const menu = this.settingsMenuRef()?.nativeElement;
11412
+ const target = event.target;
11413
+ const settingsBtn = root?.querySelector(`#${this.uid}-settings-btn`);
11414
+ if (!menu?.contains(target) && !settingsBtn?.contains(target)) {
11415
+ this.closeSettings();
11416
+ }
11417
+ }
11418
+ // ── Controls visibility ─────────────────────────────────────────────────────
11419
+ /** Show the controls and (if playing) restart the idle timer. */
11420
+ revealControls(reschedule = true) {
11421
+ if (!this.controlsVisible()) {
11422
+ this.controlsVisible.set(true);
11423
+ }
11424
+ if (reschedule) {
11425
+ this.scheduleHide();
11426
+ }
11427
+ else {
11428
+ this.clearHideTimer();
11429
+ }
11430
+ }
11431
+ scheduleHide(delay = IDLE_HIDE_DELAY_MS) {
11432
+ this.clearHideTimer();
11433
+ if (!this.isBrowser || !this.playing())
11434
+ return;
11435
+ this.zone.runOutsideAngular(() => {
11436
+ this.hideTimer = setTimeout(() => {
11437
+ this.hideTimer = null;
11438
+ if (!this.playing() || this.settingsOpen() || this.isSeeking())
11439
+ return;
11440
+ // Keep controls visible while a control (not the root itself) has focus.
11441
+ const root = this.rootRef()?.nativeElement;
11442
+ const active = document.activeElement;
11443
+ if (root && active && active !== root && root.contains(active))
11444
+ return;
11445
+ this.zone.run(() => this.controlsVisible.set(false));
11446
+ }, delay);
11447
+ });
11448
+ }
11449
+ clearHideTimer() {
11450
+ if (this.hideTimer !== null) {
11451
+ clearTimeout(this.hideTimer);
11452
+ this.hideTimer = null;
11453
+ }
11454
+ }
11455
+ /**
11456
+ * High-frequency pointer listeners are registered outside the Angular zone so
11457
+ * mouse movement over the stage doesn't trigger change detection; we only
11458
+ * re-enter the zone when the visibility state actually flips.
11459
+ */
11460
+ attachIdleListeners() {
11461
+ const root = this.rootRef()?.nativeElement;
11462
+ if (!root)
11463
+ return;
11464
+ this.zone.runOutsideAngular(() => {
11465
+ const onMove = () => {
11466
+ if (this.controlsVisible()) {
11467
+ this.scheduleHide();
11468
+ }
11469
+ else {
11470
+ this.zone.run(() => this.revealControls());
11471
+ }
11472
+ };
11473
+ const onLeave = () => {
11474
+ if (this.playing())
11475
+ this.scheduleHide(LEAVE_HIDE_DELAY_MS);
11476
+ };
11477
+ root.addEventListener('pointermove', onMove, { passive: true });
11478
+ root.addEventListener('pointerleave', onLeave, { passive: true });
11479
+ this.destroyRef.onDestroy(() => {
11480
+ root.removeEventListener('pointermove', onMove);
11481
+ root.removeEventListener('pointerleave', onLeave);
11482
+ });
11483
+ });
11484
+ }
11485
+ // ── Helpers ─────────────────────────────────────────────────────────────────
11486
+ safePlay(video) {
11487
+ if (this.pendingPlay)
11488
+ return;
11489
+ const attempt = video.play();
11490
+ if (!attempt || typeof attempt.then !== 'function')
11491
+ return;
11492
+ this.pendingPlay = attempt
11493
+ .catch((err) => {
11494
+ this.playing.set(false);
11495
+ this.playerError.emit({
11496
+ code: 'play-rejected',
11497
+ message: err instanceof Error ? err.message : 'Playback was rejected',
11498
+ originalError: err,
11499
+ });
11500
+ })
11501
+ .finally(() => {
11502
+ this.pendingPlay = null;
11503
+ });
11504
+ }
11505
+ applyCaptionMode(video, enabled) {
11506
+ const list = video.textTracks;
11507
+ if (!list || list.length === 0)
11508
+ return;
11509
+ const configured = this.tracks();
11510
+ const defaultIndex = Math.max(0, configured.findIndex((t) => t.default));
11511
+ for (let i = 0; i < list.length; i++) {
11512
+ const track = list[i];
11513
+ if (track.kind !== 'captions' && track.kind !== 'subtitles')
11514
+ continue;
11515
+ track.mode = enabled && i === defaultIndex ? 'showing' : 'hidden';
11516
+ }
11517
+ }
11518
+ resetForNewSource(video) {
11519
+ this.hasError.set(false);
11520
+ this.hasEnded.set(false);
11521
+ this.hasStarted.set(false);
11522
+ this.isBuffering.set(false);
11523
+ this.bufferedEnd.set(0);
11524
+ this.duration.set(0);
11525
+ this.startTimeApplied = false;
11526
+ // Adopt the new source's start position straight away (it is re-applied for
11527
+ // real once metadata lands) so consumers driving `src` + `startTime`
11528
+ // together — a course player switching lessons — never flash 0:00.
11529
+ this.currentTime.set(Math.max(0, this.startTime()));
11530
+ video.load();
11531
+ if (this.playing()) {
11532
+ this.safePlay(video);
11533
+ }
11534
+ }
11535
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: VideoPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
11536
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: VideoPlayerComponent, isStandalone: true, selector: "pecb-video-player", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, poster: { classPropertyName: "poster", publicName: "poster", isSignal: true, isRequired: false, transformFunction: null }, tracks: { classPropertyName: "tracks", publicName: "tracks", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, crossOrigin: { classPropertyName: "crossOrigin", publicName: "crossOrigin", isSignal: true, isRequired: false, transformFunction: null }, preload: { classPropertyName: "preload", publicName: "preload", isSignal: true, isRequired: false, transformFunction: null }, autoplay: { classPropertyName: "autoplay", publicName: "autoplay", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, playsInline: { classPropertyName: "playsInline", publicName: "playsInline", isSignal: true, isRequired: false, transformFunction: null }, startTime: { classPropertyName: "startTime", publicName: "startTime", isSignal: true, isRequired: false, transformFunction: null }, watermark: { classPropertyName: "watermark", publicName: "watermark", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, showCenterButton: { classPropertyName: "showCenterButton", publicName: "showCenterButton", isSignal: true, isRequired: false, transformFunction: null }, showSkipBack: { classPropertyName: "showSkipBack", publicName: "showSkipBack", isSignal: true, isRequired: false, transformFunction: null }, showVolume: { classPropertyName: "showVolume", publicName: "showVolume", isSignal: true, isRequired: false, transformFunction: null }, showTime: { classPropertyName: "showTime", publicName: "showTime", isSignal: true, isRequired: false, transformFunction: null }, showCaptions: { classPropertyName: "showCaptions", publicName: "showCaptions", isSignal: true, isRequired: false, transformFunction: null }, showSettings: { classPropertyName: "showSettings", publicName: "showSettings", isSignal: true, isRequired: false, transformFunction: null }, showFullscreen: { classPropertyName: "showFullscreen", publicName: "showFullscreen", isSignal: true, isRequired: false, transformFunction: null }, seekStep: { classPropertyName: "seekStep", publicName: "seekStep", isSignal: true, isRequired: false, transformFunction: null }, playbackRates: { classPropertyName: "playbackRates", publicName: "playbackRates", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, muted: { classPropertyName: "muted", publicName: "muted", isSignal: true, isRequired: false, transformFunction: null }, playbackRate: { classPropertyName: "playbackRate", publicName: "playbackRate", isSignal: true, isRequired: false, transformFunction: null }, captionsEnabled: { classPropertyName: "captionsEnabled", publicName: "captionsEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playing: "playingChange", currentTime: "currentTimeChange", volume: "volumeChange", muted: "mutedChange", playbackRate: "playbackRateChange", captionsEnabled: "captionsEnabledChange", timeUpdate: "timeUpdate", loadedMetadata: "loadedMetadata", seekEnd: "seekEnd", playbackEnded: "playbackEnded", fullscreenChange: "fullscreenChange", playerError: "playerError" }, host: { listeners: { "document:fullscreenchange": "onDocumentFullscreenChange()", "document:pointerdown": "onDocumentPointerDown($event)" }, properties: { "attr.title": "null", "style.--pecb-video-aspect-ratio": "aspectRatio()" }, classAttribute: "pecb-video-player-host" }, viewQueries: [{ propertyName: "rootRef", first: true, predicate: ["root"], descendants: true, isSignal: true }, { propertyName: "videoRef", first: true, predicate: ["video"], descendants: true, isSignal: true }, { propertyName: "progressRef", first: true, predicate: ["progress"], descendants: true, isSignal: true }, { propertyName: "settingsMenuRef", first: true, predicate: ["settingsMenu"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #root\n [class]=\"hostClasses()\"\n role=\"region\"\n [attr.aria-label]=\"regionLabel()\"\n aria-roledescription=\"video player\"\n tabindex=\"0\"\n (keydown)=\"onRootKeydown($event)\"\n (focusout)=\"onRootFocusOut($event)\"\n>\n <!-- \u2500\u2500 Media \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <video\n #video\n class=\"pecb-video-player__media\"\n [attr.src]=\"singleSrc() ?? null\"\n [attr.poster]=\"poster() ?? null\"\n [attr.preload]=\"preload()\"\n [attr.crossorigin]=\"crossOrigin() ?? null\"\n [attr.playsinline]=\"playsInline() ? '' : null\"\n [attr.webkit-playsinline]=\"playsInline() ? '' : null\"\n [attr.autoplay]=\"autoplay() ? '' : null\"\n [attr.loop]=\"loop() ? '' : null\"\n [attr.aria-label]=\"title() ?? null\"\n (loadedmetadata)=\"onLoadedMetadata(video)\"\n (durationchange)=\"onDurationChange(video)\"\n (timeupdate)=\"onTimeUpdate(video)\"\n (progress)=\"onProgress(video)\"\n (play)=\"onPlay()\"\n (pause)=\"onPause()\"\n (ended)=\"onEnded()\"\n (waiting)=\"onWaiting()\"\n (playing)=\"onPlaying()\"\n (seeked)=\"onSeeked(video)\"\n (volumechange)=\"onVolumeChange(video)\"\n (ratechange)=\"onRateChange(video)\"\n (error)=\"onMediaError(video)\"\n (pointerdown)=\"onStagePointerDown($event)\"\n (click)=\"onStageClick()\"\n (dblclick)=\"onStageDoubleClick()\"\n >\n @if (!singleSrc()) {\n @for (s of sourceList(); track s.src) {\n <source [attr.src]=\"s.src\" [attr.type]=\"s.type ?? null\" [attr.media]=\"s.media ?? null\" />\n }\n }\n @for (t of tracks(); track t.src) {\n <track\n [attr.src]=\"t.src\"\n [attr.kind]=\"t.kind ?? 'captions'\"\n [attr.srclang]=\"t.srclang\"\n [attr.label]=\"t.label ?? null\"\n [attr.default]=\"t.default ? '' : null\"\n />\n }\n </video>\n\n <!-- \u2500\u2500 Gradient scrim + branding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-video-player__scrim\" aria-hidden=\"true\"></div>\n\n @if (watermark()) {\n <div class=\"pecb-video-player__watermark\" aria-hidden=\"true\">{{ watermark() }}</div>\n }\n @if (label()) {\n <div class=\"pecb-video-player__label\" aria-hidden=\"true\">{{ label() }}</div>\n }\n\n <!-- Custom overlays (quizzes, end screens\u2026) projected by the consumer -->\n <ng-content select=\"[pecbVideoOverlay]\"></ng-content>\n\n <!-- \u2500\u2500 Centre state: error / buffering / play-pause \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (hasError()) {\n <div class=\"pecb-video-player__error\" role=\"alert\">\n <svg class=\"pecb-video-player__error-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 8v4.5\" />\n <circle cx=\"12\" cy=\"16\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n <span>{{ text().error }}</span>\n </div>\n } @else if (isBuffering() && playing()) {\n <div class=\"pecb-video-player__spinner\" aria-hidden=\"true\"></div>\n } @else if (showCenterButton()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__center\"\n [attr.aria-label]=\"hasEnded() ? text().replay : playing() ? text().pause : text().play\"\n (click)=\"togglePlay()\"\n >\n @if (hasEnded()) {\n <svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3 12a9 9 0 1 0 3-6.7\" />\n <path d=\"M3 4v5h5\" />\n </svg>\n } @else if (playing()) {\n <svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else {\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </button>\n }\n\n <!-- \u2500\u2500 Control bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-video-player__controls\" [attr.aria-hidden]=\"controlsHidden() ? 'true' : null\">\n <!-- Progress / seek bar -->\n <div\n #progress\n class=\"pecb-video-player__progress\"\n role=\"slider\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n [attr.aria-label]=\"text().seek\"\n aria-valuemin=\"0\"\n [attr.aria-valuemax]=\"duration()\"\n [attr.aria-valuenow]=\"currentTime()\"\n [attr.aria-valuetext]=\"currentTimeLabel() + ' / ' + durationLabel()\"\n [attr.aria-disabled]=\"duration() > 0 ? null : 'true'\"\n (pointerdown)=\"onProgressPointerDown($event)\"\n (pointermove)=\"onProgressPointerMove($event)\"\n (pointerup)=\"onProgressPointerUp($event)\"\n (pointercancel)=\"onProgressPointerUp($event)\"\n (keydown)=\"onProgressKeydown($event)\"\n >\n <div class=\"pecb-video-player__track\">\n <span class=\"pecb-video-player__buffered\" [style.width.%]=\"bufferedPct()\"></span>\n <span class=\"pecb-video-player__fill\" [style.width.%]=\"progressPct()\"></span>\n <span class=\"pecb-video-player__knob\" [style.left.%]=\"progressPct()\"></span>\n </div>\n </div>\n\n <!-- Buttons row -->\n <div class=\"pecb-video-player__bar\">\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"hasEnded() ? text().replay : playing() ? text().pause : text().play\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"togglePlay()\"\n >\n @if (playing()) {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </button>\n\n @if (showSkipBack()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"skipBackLabel()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"skipBack()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"19 5 9 12 19 19 19 5\" fill=\"currentColor\" stroke=\"none\" />\n <line x1=\"6\" y1=\"5\" x2=\"6\" y2=\"19\" />\n </svg>\n </button>\n }\n\n @if (showVolume()) {\n <div class=\"pecb-video-player__volume\">\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"muted() ? text().unmute : text().mute\"\n [attr.aria-pressed]=\"muted()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleMute()\"\n >\n @switch (volumeLevel()) {\n @case ('muted') {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4 9 8 9 12 5 12 19 8 15 4 15 4 9\" fill=\"currentColor\" stroke=\"none\" />\n <path d=\"m16 9.5 5 5M21 9.5l-5 5\" />\n </svg>\n }\n @case ('low') {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4 9 8 9 12 5 12 19 8 15 4 15 4 9\" fill=\"currentColor\" stroke=\"none\" />\n <path d=\"M16 9a4 4 0 0 1 0 6\" />\n </svg>\n }\n @default {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4 9 8 9 12 5 12 19 8 15 4 15 4 9\" fill=\"currentColor\" stroke=\"none\" />\n <path d=\"M16 9a4 4 0 0 1 0 6\" />\n <path d=\"M18.5 6.5a8 8 0 0 1 0 11\" />\n </svg>\n }\n }\n </button>\n <input\n class=\"pecb-video-player__volume-slider\"\n type=\"range\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n [value]=\"volumePct()\"\n [attr.aria-label]=\"text().volume\"\n [attr.aria-valuetext]=\"volumePct() + '%'\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n [style.--pecb-video-volume-pct]=\"volumePct() + '%'\"\n (input)=\"onVolumeInput($event)\"\n />\n </div>\n }\n\n @if (showTime()) {\n <span class=\"pecb-video-player__time\" aria-live=\"off\">\n <span class=\"pecb-video-player__time-current\">{{ currentTimeLabel() }}</span>\n <span class=\"pecb-video-player__time-sep\" aria-hidden=\"true\"> / </span>\n <span class=\"pecb-video-player__time-total\">{{ durationLabel() }}</span>\n </span>\n }\n\n <div class=\"pecb-video-player__spacer\"></div>\n\n @if (captionsButtonVisible()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [class.pecb-video-player__btn--active]=\"captionsEnabled()\"\n [attr.aria-label]=\"captionsEnabled() ? text().captionsOff : text().captionsOn\"\n [attr.aria-pressed]=\"captionsEnabled()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleCaptions()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <path d=\"M10 10a2 2 0 1 0 0 4M17 10a2 2 0 1 0 0 4\" />\n </svg>\n </button>\n }\n\n @if (showSettings()) {\n <div class=\"pecb-video-player__settings\">\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [id]=\"uid + '-settings-btn'\"\n [class.pecb-video-player__btn--active]=\"settingsOpen()\"\n [attr.aria-label]=\"text().settings\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"settingsOpen()\"\n [attr.aria-controls]=\"uid + '-settings-menu'\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleSettings()\"\n >\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n <path d=\"M19.4 13.5a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 0 1-4 0v-.2A1.6 1.6 0 0 0 6.6 19l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1A1.6 1.6 0 0 0 3 13.5H3a2 2 0 0 1 0-4h.1a1.6 1.6 0 0 0 1.5-1.1 1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3H9.6A1.6 1.6 0 0 0 10.6 3V3a2 2 0 0 1 4 0v.1a1.6 1.6 0 0 0 2.7 1.1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8v.1a1.6 1.6 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z\" />\n </svg>\n </button>\n\n @if (settingsOpen()) {\n <div\n #settingsMenu\n class=\"pecb-video-player__menu\"\n role=\"menu\"\n [id]=\"uid + '-settings-menu'\"\n [attr.aria-label]=\"text().playbackSpeed\"\n (keydown)=\"onSettingsMenuKeydown($event)\"\n >\n <div class=\"pecb-video-player__menu-title\">{{ text().playbackSpeed }}</div>\n @for (rate of playbackRates(); track rate) {\n <button\n type=\"button\"\n class=\"pecb-video-player__menu-item\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"rate === playbackRate()\"\n tabindex=\"-1\"\n (click)=\"setPlaybackRate(rate)\"\n >\n <span class=\"pecb-video-player__menu-check\" aria-hidden=\"true\">\n @if (rate === playbackRate()) {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n </span>\n <span>{{ rate === 1 ? text().normalSpeed : rate + '\u00D7' }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showFullscreen()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"isFullscreen() ? text().exitFullscreen : text().enterFullscreen\"\n [attr.aria-pressed]=\"isFullscreen()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleFullscreen()\"\n >\n @if (isFullscreen()) {\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3m13-5h-3a2 2 0 0 0-2 2v3\" />\n </svg>\n } @else {\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3\" />\n </svg>\n }\n </button>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;max-width:100%;container-type:inline-size;container-name:pecb-video-player;--pecb-video-radius: 12px;--pecb-video-bg: #0b0b0d;--pecb-video-accent: #ffffff;--pecb-video-controls-color: rgba(255, 255, 255, .92);--pecb-video-font-display: Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;--pecb-video-font-body: \"Inter\", Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;--pecb-video-focus-ring: #ffffff}.pecb-video-player{position:relative;width:100%;aspect-ratio:var(--pecb-video-aspect-ratio, 16/9);max-height:100%;background:var(--pecb-video-bg);border-radius:var(--pecb-video-radius);overflow:hidden;color:var(--pecb-video-controls-color);font-family:var(--pecb-video-font-display);outline:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none}.pecb-video-player:focus-visible{box-shadow:0 0 0 2px var(--pecb-video-bg),0 0 0 4px var(--pecb-video-focus-ring)}.pecb-video-player__media{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:var(--pecb-video-bg);display:block;cursor:pointer}.pecb-video-player__media::cue{font-family:var(--pecb-video-font-body);font-size:.95em;background:#0009}.pecb-video-player__scrim{position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#0000001f,#0000 30% 60%,#0000008c)}.pecb-video-player__watermark,.pecb-video-player__label{position:absolute;bottom:64px;pointer-events:none;font-family:var(--pecb-video-font-display);font-weight:800;color:#ffffffeb;white-space:nowrap;max-width:45%;overflow:hidden;text-overflow:ellipsis;text-shadow:0 1px 2px rgba(0,0,0,.25);transition:opacity .22s ease}.pecb-video-player__watermark{left:24px;font-size:22px;letter-spacing:.04em}.pecb-video-player__label{right:24px;font-size:26px;letter-spacing:.05em;text-transform:uppercase}.pecb-video-player__center{padding:0;border:none;background:none;font:inherit;color:inherit;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;border-radius:50%;background:#1414168c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s ease-in-out,transform .15s ease-in-out,opacity .22s ease}.pecb-video-player__center:focus{outline:none}.pecb-video-player__center:hover{background:#141416b8}.pecb-video-player__center:active{transform:translate(-50%,-50%) scale(.96)}.pecb-video-player__center:focus-visible{outline:2px solid var(--pecb-video-focus-ring);outline-offset:2px}.pecb-video-player__center svg polygon{transform:translate(1px)}.pecb-video-player__spinner{position:absolute;left:50%;top:50%;width:48px;height:48px;margin:-24px 0 0 -24px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:#fff;animation:pecb-video-spin .9s linear infinite;pointer-events:none}.pecb-video-player__error{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px;text-align:center;font-family:var(--pecb-video-font-body);font-size:14px;color:#ffffffd9;background:#00000073}.pecb-video-player__error-icon{width:36px;height:36px;opacity:.9}.pecb-video-player__controls{position:absolute;left:0;right:0;bottom:0;display:flex;flex-direction:column;gap:8px;padding:10px 16px 12px;background:linear-gradient(0deg,#0009,#0000);transition:opacity .22s ease,transform .22s ease}.pecb-video-player__progress{position:relative;padding:6px 0;margin:-6px 0;cursor:pointer;touch-action:none;border-radius:6px;outline:none}.pecb-video-player__progress[aria-disabled=true]{cursor:default}.pecb-video-player__progress:focus-visible .pecb-video-player__track{box-shadow:0 0 0 2px #0009,0 0 0 4px var(--pecb-video-focus-ring)}.pecb-video-player__progress:hover .pecb-video-player__knob,.pecb-video-player__progress:focus-visible .pecb-video-player__knob{transform:translate(-50%,-50%) scale(1.15)}.pecb-video-player__track{position:relative;height:4px;border-radius:4px;background:#ffffff47;overflow:visible}.pecb-video-player__buffered,.pecb-video-player__fill{position:absolute;left:0;top:0;bottom:0;border-radius:4px;pointer-events:none}.pecb-video-player__buffered{background:#ffffff38}.pecb-video-player__fill{background:var(--pecb-video-accent)}.pecb-video-player__knob{position:absolute;top:50%;width:11px;height:11px;border-radius:50%;background:var(--pecb-video-accent);transform:translate(-50%,-50%);pointer-events:none;box-shadow:0 1px 3px #00000059;transition:transform .15s ease-in-out}.pecb-video-player__bar{display:flex;align-items:center;gap:6px;color:var(--pecb-video-controls-color);min-height:28px}.pecb-video-player__btn{padding:0;border:none;background:none;font:inherit;flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;color:inherit;opacity:.92;cursor:pointer;transition:opacity .15s ease-in-out,background .15s ease-in-out}.pecb-video-player__btn:focus{outline:none}.pecb-video-player__btn:hover{opacity:1;background:#ffffff1a}.pecb-video-player__btn:focus-visible{opacity:1;outline:2px solid var(--pecb-video-focus-ring);outline-offset:1px}.pecb-video-player__btn--active{opacity:1;position:relative}.pecb-video-player__btn--active:after{content:\"\";position:absolute;left:8px;right:8px;bottom:2px;height:2px;border-radius:2px;background:var(--pecb-video-accent)}.pecb-video-player__btn svg{display:block;flex:0 0 auto}.pecb-video-player__spacer{flex:1 1 auto;min-width:0}.pecb-video-player__time{font-family:var(--pecb-video-font-body);font-size:12.5px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;margin-left:4px;opacity:.95}.pecb-video-player__volume{display:flex;align-items:center;flex:0 0 auto}.pecb-video-player__volume-slider{width:0;opacity:0;margin:0;height:28px;cursor:pointer;appearance:none;-webkit-appearance:none;background:transparent;transition:width .15s ease-in-out,opacity .15s ease-in-out,margin .15s ease-in-out}.pecb-video-player__volume-slider:focus-visible{outline:2px solid var(--pecb-video-focus-ring);outline-offset:1px;border-radius:6px}.pecb-video-player__volume-slider::-webkit-slider-runnable-track{height:4px;border-radius:4px;background:linear-gradient(to right,var(--pecb-video-accent) 0 var(--pecb-video-volume-pct, 100%),rgba(255,255,255,.28) var(--pecb-video-volume-pct, 100%) 100%)}.pecb-video-player__volume-slider::-moz-range-track{height:4px;border-radius:4px;background:#ffffff47}.pecb-video-player__volume-slider::-moz-range-progress{height:4px;border-radius:4px;background:var(--pecb-video-accent)}.pecb-video-player__volume-slider::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:11px;margin-top:-3.5px;border-radius:50%;border:none;background:var(--pecb-video-accent)}.pecb-video-player__volume-slider::-moz-range-thumb{width:11px;height:11px;border-radius:50%;border:none;background:var(--pecb-video-accent)}.pecb-video-player__volume:hover .pecb-video-player__volume-slider,.pecb-video-player__volume:focus-within .pecb-video-player__volume-slider{width:72px;opacity:1;margin-left:4px;margin-right:6px}.pecb-video-player__settings{position:relative;flex:0 0 auto}.pecb-video-player__menu{position:absolute;right:0;bottom:calc(100% + 10px);min-width:168px;padding:6px;border-radius:8px;background:#18181bf5;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d;color:#fff;z-index:2;font-family:var(--pecb-video-font-body);animation:pecb-video-menu-in .14s ease-out}.pecb-video-player__menu-title{padding:6px 10px 8px;font-size:12px;font-weight:500;color:#fff9;letter-spacing:.02em}.pecb-video-player__menu-item{border:none;background:none;font:inherit;display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border-radius:6px;font-size:13px;font-weight:500;color:inherit;text-align:left;cursor:pointer}.pecb-video-player__menu-item:focus{outline:none}.pecb-video-player__menu-item:hover,.pecb-video-player__menu-item:focus-visible{background:#ffffff1a;outline:none}.pecb-video-player__menu-item[aria-checked=true]{font-weight:700}.pecb-video-player__menu-check{width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}.pecb-video-player--idle,.pecb-video-player--idle .pecb-video-player__media{cursor:none}.pecb-video-player--idle .pecb-video-player__controls,.pecb-video-player--idle .pecb-video-player__center{opacity:0;pointer-events:none}.pecb-video-player--idle .pecb-video-player__controls{transform:translateY(6px)}.pecb-video-player--fullscreen{width:100%;height:100%;aspect-ratio:auto;border-radius:0;max-height:none}.pecb-video-player--fullscreen .pecb-video-player__media{object-fit:contain}.pecb-video-player--error .pecb-video-player__media{cursor:default}@container pecb-video-player (max-width: 640px){.pecb-video-player__watermark,.pecb-video-player__label{bottom:56px}.pecb-video-player__watermark{left:16px;font-size:17px}.pecb-video-player__label{right:16px;font-size:20px}.pecb-video-player__center{width:54px;height:54px}.pecb-video-player__center svg{width:22px;height:22px}.pecb-video-player__controls{padding:8px 12px 10px;gap:6px}.pecb-video-player__bar{gap:4px}.pecb-video-player__time{font-size:11.5px}.pecb-video-player__volume:hover .pecb-video-player__volume-slider,.pecb-video-player__volume:focus-within .pecb-video-player__volume-slider{width:56px}}@container pecb-video-player (max-width: 400px){.pecb-video-player__watermark,.pecb-video-player__label{bottom:50px}.pecb-video-player__watermark{font-size:14px}.pecb-video-player__label{font-size:16px}.pecb-video-player__center{width:46px;height:46px}.pecb-video-player__center svg{width:19px;height:19px}.pecb-video-player__bar{gap:2px}.pecb-video-player__time{font-size:11px;margin-left:2px}.pecb-video-player__time-sep,.pecb-video-player__time-total{display:none}.pecb-video-player__menu{min-width:148px}}@media (hover: none) and (pointer: coarse){.pecb-video-player__volume-slider{display:none}}@media (prefers-reduced-motion: reduce){.pecb-video-player,.pecb-video-player *,.pecb-video-player *:before,.pecb-video-player *:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}.pecb-video-player__spinner{animation:none;border-top-color:#ffffff40}}@keyframes pecb-video-spin{to{transform:rotate(360deg)}}@keyframes pecb-video-menu-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
11537
+ }
11538
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: VideoPlayerComponent, decorators: [{
11539
+ type: Component,
11540
+ args: [{ selector: 'pecb-video-player', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, host: {
11541
+ class: 'pecb-video-player-host',
11542
+ // `title` is a component input — keep it off the host so the browser doesn't
11543
+ // show a native tooltip over the whole stage.
11544
+ '[attr.title]': 'null',
11545
+ '[style.--pecb-video-aspect-ratio]': 'aspectRatio()',
11546
+ '(document:fullscreenchange)': 'onDocumentFullscreenChange()',
11547
+ '(document:pointerdown)': 'onDocumentPointerDown($event)',
11548
+ }, template: "<div\n #root\n [class]=\"hostClasses()\"\n role=\"region\"\n [attr.aria-label]=\"regionLabel()\"\n aria-roledescription=\"video player\"\n tabindex=\"0\"\n (keydown)=\"onRootKeydown($event)\"\n (focusout)=\"onRootFocusOut($event)\"\n>\n <!-- \u2500\u2500 Media \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <video\n #video\n class=\"pecb-video-player__media\"\n [attr.src]=\"singleSrc() ?? null\"\n [attr.poster]=\"poster() ?? null\"\n [attr.preload]=\"preload()\"\n [attr.crossorigin]=\"crossOrigin() ?? null\"\n [attr.playsinline]=\"playsInline() ? '' : null\"\n [attr.webkit-playsinline]=\"playsInline() ? '' : null\"\n [attr.autoplay]=\"autoplay() ? '' : null\"\n [attr.loop]=\"loop() ? '' : null\"\n [attr.aria-label]=\"title() ?? null\"\n (loadedmetadata)=\"onLoadedMetadata(video)\"\n (durationchange)=\"onDurationChange(video)\"\n (timeupdate)=\"onTimeUpdate(video)\"\n (progress)=\"onProgress(video)\"\n (play)=\"onPlay()\"\n (pause)=\"onPause()\"\n (ended)=\"onEnded()\"\n (waiting)=\"onWaiting()\"\n (playing)=\"onPlaying()\"\n (seeked)=\"onSeeked(video)\"\n (volumechange)=\"onVolumeChange(video)\"\n (ratechange)=\"onRateChange(video)\"\n (error)=\"onMediaError(video)\"\n (pointerdown)=\"onStagePointerDown($event)\"\n (click)=\"onStageClick()\"\n (dblclick)=\"onStageDoubleClick()\"\n >\n @if (!singleSrc()) {\n @for (s of sourceList(); track s.src) {\n <source [attr.src]=\"s.src\" [attr.type]=\"s.type ?? null\" [attr.media]=\"s.media ?? null\" />\n }\n }\n @for (t of tracks(); track t.src) {\n <track\n [attr.src]=\"t.src\"\n [attr.kind]=\"t.kind ?? 'captions'\"\n [attr.srclang]=\"t.srclang\"\n [attr.label]=\"t.label ?? null\"\n [attr.default]=\"t.default ? '' : null\"\n />\n }\n </video>\n\n <!-- \u2500\u2500 Gradient scrim + branding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-video-player__scrim\" aria-hidden=\"true\"></div>\n\n @if (watermark()) {\n <div class=\"pecb-video-player__watermark\" aria-hidden=\"true\">{{ watermark() }}</div>\n }\n @if (label()) {\n <div class=\"pecb-video-player__label\" aria-hidden=\"true\">{{ label() }}</div>\n }\n\n <!-- Custom overlays (quizzes, end screens\u2026) projected by the consumer -->\n <ng-content select=\"[pecbVideoOverlay]\"></ng-content>\n\n <!-- \u2500\u2500 Centre state: error / buffering / play-pause \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (hasError()) {\n <div class=\"pecb-video-player__error\" role=\"alert\">\n <svg class=\"pecb-video-player__error-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 8v4.5\" />\n <circle cx=\"12\" cy=\"16\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n <span>{{ text().error }}</span>\n </div>\n } @else if (isBuffering() && playing()) {\n <div class=\"pecb-video-player__spinner\" aria-hidden=\"true\"></div>\n } @else if (showCenterButton()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__center\"\n [attr.aria-label]=\"hasEnded() ? text().replay : playing() ? text().pause : text().play\"\n (click)=\"togglePlay()\"\n >\n @if (hasEnded()) {\n <svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3 12a9 9 0 1 0 3-6.7\" />\n <path d=\"M3 4v5h5\" />\n </svg>\n } @else if (playing()) {\n <svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else {\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </button>\n }\n\n <!-- \u2500\u2500 Control bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-video-player__controls\" [attr.aria-hidden]=\"controlsHidden() ? 'true' : null\">\n <!-- Progress / seek bar -->\n <div\n #progress\n class=\"pecb-video-player__progress\"\n role=\"slider\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n [attr.aria-label]=\"text().seek\"\n aria-valuemin=\"0\"\n [attr.aria-valuemax]=\"duration()\"\n [attr.aria-valuenow]=\"currentTime()\"\n [attr.aria-valuetext]=\"currentTimeLabel() + ' / ' + durationLabel()\"\n [attr.aria-disabled]=\"duration() > 0 ? null : 'true'\"\n (pointerdown)=\"onProgressPointerDown($event)\"\n (pointermove)=\"onProgressPointerMove($event)\"\n (pointerup)=\"onProgressPointerUp($event)\"\n (pointercancel)=\"onProgressPointerUp($event)\"\n (keydown)=\"onProgressKeydown($event)\"\n >\n <div class=\"pecb-video-player__track\">\n <span class=\"pecb-video-player__buffered\" [style.width.%]=\"bufferedPct()\"></span>\n <span class=\"pecb-video-player__fill\" [style.width.%]=\"progressPct()\"></span>\n <span class=\"pecb-video-player__knob\" [style.left.%]=\"progressPct()\"></span>\n </div>\n </div>\n\n <!-- Buttons row -->\n <div class=\"pecb-video-player__bar\">\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"hasEnded() ? text().replay : playing() ? text().pause : text().play\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"togglePlay()\"\n >\n @if (playing()) {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </button>\n\n @if (showSkipBack()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"skipBackLabel()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"skipBack()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"19 5 9 12 19 19 19 5\" fill=\"currentColor\" stroke=\"none\" />\n <line x1=\"6\" y1=\"5\" x2=\"6\" y2=\"19\" />\n </svg>\n </button>\n }\n\n @if (showVolume()) {\n <div class=\"pecb-video-player__volume\">\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"muted() ? text().unmute : text().mute\"\n [attr.aria-pressed]=\"muted()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleMute()\"\n >\n @switch (volumeLevel()) {\n @case ('muted') {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4 9 8 9 12 5 12 19 8 15 4 15 4 9\" fill=\"currentColor\" stroke=\"none\" />\n <path d=\"m16 9.5 5 5M21 9.5l-5 5\" />\n </svg>\n }\n @case ('low') {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4 9 8 9 12 5 12 19 8 15 4 15 4 9\" fill=\"currentColor\" stroke=\"none\" />\n <path d=\"M16 9a4 4 0 0 1 0 6\" />\n </svg>\n }\n @default {\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4 9 8 9 12 5 12 19 8 15 4 15 4 9\" fill=\"currentColor\" stroke=\"none\" />\n <path d=\"M16 9a4 4 0 0 1 0 6\" />\n <path d=\"M18.5 6.5a8 8 0 0 1 0 11\" />\n </svg>\n }\n }\n </button>\n <input\n class=\"pecb-video-player__volume-slider\"\n type=\"range\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n [value]=\"volumePct()\"\n [attr.aria-label]=\"text().volume\"\n [attr.aria-valuetext]=\"volumePct() + '%'\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n [style.--pecb-video-volume-pct]=\"volumePct() + '%'\"\n (input)=\"onVolumeInput($event)\"\n />\n </div>\n }\n\n @if (showTime()) {\n <span class=\"pecb-video-player__time\" aria-live=\"off\">\n <span class=\"pecb-video-player__time-current\">{{ currentTimeLabel() }}</span>\n <span class=\"pecb-video-player__time-sep\" aria-hidden=\"true\"> / </span>\n <span class=\"pecb-video-player__time-total\">{{ durationLabel() }}</span>\n </span>\n }\n\n <div class=\"pecb-video-player__spacer\"></div>\n\n @if (captionsButtonVisible()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [class.pecb-video-player__btn--active]=\"captionsEnabled()\"\n [attr.aria-label]=\"captionsEnabled() ? text().captionsOff : text().captionsOn\"\n [attr.aria-pressed]=\"captionsEnabled()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleCaptions()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <path d=\"M10 10a2 2 0 1 0 0 4M17 10a2 2 0 1 0 0 4\" />\n </svg>\n </button>\n }\n\n @if (showSettings()) {\n <div class=\"pecb-video-player__settings\">\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [id]=\"uid + '-settings-btn'\"\n [class.pecb-video-player__btn--active]=\"settingsOpen()\"\n [attr.aria-label]=\"text().settings\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"settingsOpen()\"\n [attr.aria-controls]=\"uid + '-settings-menu'\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleSettings()\"\n >\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n <path d=\"M19.4 13.5a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 0 1-4 0v-.2A1.6 1.6 0 0 0 6.6 19l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1A1.6 1.6 0 0 0 3 13.5H3a2 2 0 0 1 0-4h.1a1.6 1.6 0 0 0 1.5-1.1 1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3H9.6A1.6 1.6 0 0 0 10.6 3V3a2 2 0 0 1 4 0v.1a1.6 1.6 0 0 0 2.7 1.1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8v.1a1.6 1.6 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z\" />\n </svg>\n </button>\n\n @if (settingsOpen()) {\n <div\n #settingsMenu\n class=\"pecb-video-player__menu\"\n role=\"menu\"\n [id]=\"uid + '-settings-menu'\"\n [attr.aria-label]=\"text().playbackSpeed\"\n (keydown)=\"onSettingsMenuKeydown($event)\"\n >\n <div class=\"pecb-video-player__menu-title\">{{ text().playbackSpeed }}</div>\n @for (rate of playbackRates(); track rate) {\n <button\n type=\"button\"\n class=\"pecb-video-player__menu-item\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"rate === playbackRate()\"\n tabindex=\"-1\"\n (click)=\"setPlaybackRate(rate)\"\n >\n <span class=\"pecb-video-player__menu-check\" aria-hidden=\"true\">\n @if (rate === playbackRate()) {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n </span>\n <span>{{ rate === 1 ? text().normalSpeed : rate + '\u00D7' }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showFullscreen()) {\n <button\n type=\"button\"\n class=\"pecb-video-player__btn\"\n [attr.aria-label]=\"isFullscreen() ? text().exitFullscreen : text().enterFullscreen\"\n [attr.aria-pressed]=\"isFullscreen()\"\n [attr.tabindex]=\"controlsHidden() ? -1 : 0\"\n (click)=\"toggleFullscreen()\"\n >\n @if (isFullscreen()) {\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3m13-5h-3a2 2 0 0 0-2 2v3\" />\n </svg>\n } @else {\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3\" />\n </svg>\n }\n </button>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;max-width:100%;container-type:inline-size;container-name:pecb-video-player;--pecb-video-radius: 12px;--pecb-video-bg: #0b0b0d;--pecb-video-accent: #ffffff;--pecb-video-controls-color: rgba(255, 255, 255, .92);--pecb-video-font-display: Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;--pecb-video-font-body: \"Inter\", Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;--pecb-video-focus-ring: #ffffff}.pecb-video-player{position:relative;width:100%;aspect-ratio:var(--pecb-video-aspect-ratio, 16/9);max-height:100%;background:var(--pecb-video-bg);border-radius:var(--pecb-video-radius);overflow:hidden;color:var(--pecb-video-controls-color);font-family:var(--pecb-video-font-display);outline:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none}.pecb-video-player:focus-visible{box-shadow:0 0 0 2px var(--pecb-video-bg),0 0 0 4px var(--pecb-video-focus-ring)}.pecb-video-player__media{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:var(--pecb-video-bg);display:block;cursor:pointer}.pecb-video-player__media::cue{font-family:var(--pecb-video-font-body);font-size:.95em;background:#0009}.pecb-video-player__scrim{position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#0000001f,#0000 30% 60%,#0000008c)}.pecb-video-player__watermark,.pecb-video-player__label{position:absolute;bottom:64px;pointer-events:none;font-family:var(--pecb-video-font-display);font-weight:800;color:#ffffffeb;white-space:nowrap;max-width:45%;overflow:hidden;text-overflow:ellipsis;text-shadow:0 1px 2px rgba(0,0,0,.25);transition:opacity .22s ease}.pecb-video-player__watermark{left:24px;font-size:22px;letter-spacing:.04em}.pecb-video-player__label{right:24px;font-size:26px;letter-spacing:.05em;text-transform:uppercase}.pecb-video-player__center{padding:0;border:none;background:none;font:inherit;color:inherit;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:64px;height:64px;border-radius:50%;background:#1414168c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s ease-in-out,transform .15s ease-in-out,opacity .22s ease}.pecb-video-player__center:focus{outline:none}.pecb-video-player__center:hover{background:#141416b8}.pecb-video-player__center:active{transform:translate(-50%,-50%) scale(.96)}.pecb-video-player__center:focus-visible{outline:2px solid var(--pecb-video-focus-ring);outline-offset:2px}.pecb-video-player__center svg polygon{transform:translate(1px)}.pecb-video-player__spinner{position:absolute;left:50%;top:50%;width:48px;height:48px;margin:-24px 0 0 -24px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:#fff;animation:pecb-video-spin .9s linear infinite;pointer-events:none}.pecb-video-player__error{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px;text-align:center;font-family:var(--pecb-video-font-body);font-size:14px;color:#ffffffd9;background:#00000073}.pecb-video-player__error-icon{width:36px;height:36px;opacity:.9}.pecb-video-player__controls{position:absolute;left:0;right:0;bottom:0;display:flex;flex-direction:column;gap:8px;padding:10px 16px 12px;background:linear-gradient(0deg,#0009,#0000);transition:opacity .22s ease,transform .22s ease}.pecb-video-player__progress{position:relative;padding:6px 0;margin:-6px 0;cursor:pointer;touch-action:none;border-radius:6px;outline:none}.pecb-video-player__progress[aria-disabled=true]{cursor:default}.pecb-video-player__progress:focus-visible .pecb-video-player__track{box-shadow:0 0 0 2px #0009,0 0 0 4px var(--pecb-video-focus-ring)}.pecb-video-player__progress:hover .pecb-video-player__knob,.pecb-video-player__progress:focus-visible .pecb-video-player__knob{transform:translate(-50%,-50%) scale(1.15)}.pecb-video-player__track{position:relative;height:4px;border-radius:4px;background:#ffffff47;overflow:visible}.pecb-video-player__buffered,.pecb-video-player__fill{position:absolute;left:0;top:0;bottom:0;border-radius:4px;pointer-events:none}.pecb-video-player__buffered{background:#ffffff38}.pecb-video-player__fill{background:var(--pecb-video-accent)}.pecb-video-player__knob{position:absolute;top:50%;width:11px;height:11px;border-radius:50%;background:var(--pecb-video-accent);transform:translate(-50%,-50%);pointer-events:none;box-shadow:0 1px 3px #00000059;transition:transform .15s ease-in-out}.pecb-video-player__bar{display:flex;align-items:center;gap:6px;color:var(--pecb-video-controls-color);min-height:28px}.pecb-video-player__btn{padding:0;border:none;background:none;font:inherit;flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;color:inherit;opacity:.92;cursor:pointer;transition:opacity .15s ease-in-out,background .15s ease-in-out}.pecb-video-player__btn:focus{outline:none}.pecb-video-player__btn:hover{opacity:1;background:#ffffff1a}.pecb-video-player__btn:focus-visible{opacity:1;outline:2px solid var(--pecb-video-focus-ring);outline-offset:1px}.pecb-video-player__btn--active{opacity:1;position:relative}.pecb-video-player__btn--active:after{content:\"\";position:absolute;left:8px;right:8px;bottom:2px;height:2px;border-radius:2px;background:var(--pecb-video-accent)}.pecb-video-player__btn svg{display:block;flex:0 0 auto}.pecb-video-player__spacer{flex:1 1 auto;min-width:0}.pecb-video-player__time{font-family:var(--pecb-video-font-body);font-size:12.5px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;margin-left:4px;opacity:.95}.pecb-video-player__volume{display:flex;align-items:center;flex:0 0 auto}.pecb-video-player__volume-slider{width:0;opacity:0;margin:0;height:28px;cursor:pointer;appearance:none;-webkit-appearance:none;background:transparent;transition:width .15s ease-in-out,opacity .15s ease-in-out,margin .15s ease-in-out}.pecb-video-player__volume-slider:focus-visible{outline:2px solid var(--pecb-video-focus-ring);outline-offset:1px;border-radius:6px}.pecb-video-player__volume-slider::-webkit-slider-runnable-track{height:4px;border-radius:4px;background:linear-gradient(to right,var(--pecb-video-accent) 0 var(--pecb-video-volume-pct, 100%),rgba(255,255,255,.28) var(--pecb-video-volume-pct, 100%) 100%)}.pecb-video-player__volume-slider::-moz-range-track{height:4px;border-radius:4px;background:#ffffff47}.pecb-video-player__volume-slider::-moz-range-progress{height:4px;border-radius:4px;background:var(--pecb-video-accent)}.pecb-video-player__volume-slider::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:11px;margin-top:-3.5px;border-radius:50%;border:none;background:var(--pecb-video-accent)}.pecb-video-player__volume-slider::-moz-range-thumb{width:11px;height:11px;border-radius:50%;border:none;background:var(--pecb-video-accent)}.pecb-video-player__volume:hover .pecb-video-player__volume-slider,.pecb-video-player__volume:focus-within .pecb-video-player__volume-slider{width:72px;opacity:1;margin-left:4px;margin-right:6px}.pecb-video-player__settings{position:relative;flex:0 0 auto}.pecb-video-player__menu{position:absolute;right:0;bottom:calc(100% + 10px);min-width:168px;padding:6px;border-radius:8px;background:#18181bf5;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d;color:#fff;z-index:2;font-family:var(--pecb-video-font-body);animation:pecb-video-menu-in .14s ease-out}.pecb-video-player__menu-title{padding:6px 10px 8px;font-size:12px;font-weight:500;color:#fff9;letter-spacing:.02em}.pecb-video-player__menu-item{border:none;background:none;font:inherit;display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border-radius:6px;font-size:13px;font-weight:500;color:inherit;text-align:left;cursor:pointer}.pecb-video-player__menu-item:focus{outline:none}.pecb-video-player__menu-item:hover,.pecb-video-player__menu-item:focus-visible{background:#ffffff1a;outline:none}.pecb-video-player__menu-item[aria-checked=true]{font-weight:700}.pecb-video-player__menu-check{width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}.pecb-video-player--idle,.pecb-video-player--idle .pecb-video-player__media{cursor:none}.pecb-video-player--idle .pecb-video-player__controls,.pecb-video-player--idle .pecb-video-player__center{opacity:0;pointer-events:none}.pecb-video-player--idle .pecb-video-player__controls{transform:translateY(6px)}.pecb-video-player--fullscreen{width:100%;height:100%;aspect-ratio:auto;border-radius:0;max-height:none}.pecb-video-player--fullscreen .pecb-video-player__media{object-fit:contain}.pecb-video-player--error .pecb-video-player__media{cursor:default}@container pecb-video-player (max-width: 640px){.pecb-video-player__watermark,.pecb-video-player__label{bottom:56px}.pecb-video-player__watermark{left:16px;font-size:17px}.pecb-video-player__label{right:16px;font-size:20px}.pecb-video-player__center{width:54px;height:54px}.pecb-video-player__center svg{width:22px;height:22px}.pecb-video-player__controls{padding:8px 12px 10px;gap:6px}.pecb-video-player__bar{gap:4px}.pecb-video-player__time{font-size:11.5px}.pecb-video-player__volume:hover .pecb-video-player__volume-slider,.pecb-video-player__volume:focus-within .pecb-video-player__volume-slider{width:56px}}@container pecb-video-player (max-width: 400px){.pecb-video-player__watermark,.pecb-video-player__label{bottom:50px}.pecb-video-player__watermark{font-size:14px}.pecb-video-player__label{font-size:16px}.pecb-video-player__center{width:46px;height:46px}.pecb-video-player__center svg{width:19px;height:19px}.pecb-video-player__bar{gap:2px}.pecb-video-player__time{font-size:11px;margin-left:2px}.pecb-video-player__time-sep,.pecb-video-player__time-total{display:none}.pecb-video-player__menu{min-width:148px}}@media (hover: none) and (pointer: coarse){.pecb-video-player__volume-slider{display:none}}@media (prefers-reduced-motion: reduce){.pecb-video-player,.pecb-video-player *,.pecb-video-player *:before,.pecb-video-player *:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}.pecb-video-player__spinner{animation:none;border-top-color:#ffffff40}}@keyframes pecb-video-spin{to{transform:rotate(360deg)}}@keyframes pecb-video-menu-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n"] }]
11549
+ }], ctorParameters: () => [], propDecorators: { rootRef: [{ type: i0.ViewChild, args: ['root', { isSignal: true }] }], videoRef: [{ type: i0.ViewChild, args: ['video', { isSignal: true }] }], progressRef: [{ type: i0.ViewChild, args: ['progress', { isSignal: true }] }], settingsMenuRef: [{ type: i0.ViewChild, args: ['settingsMenu', { isSignal: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], poster: [{ type: i0.Input, args: [{ isSignal: true, alias: "poster", required: false }] }], tracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "tracks", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], crossOrigin: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossOrigin", required: false }] }], preload: [{ type: i0.Input, args: [{ isSignal: true, alias: "preload", required: false }] }], autoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplay", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], playsInline: [{ type: i0.Input, args: [{ isSignal: true, alias: "playsInline", required: false }] }], startTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "startTime", required: false }] }], watermark: [{ type: i0.Input, args: [{ isSignal: true, alias: "watermark", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], showCenterButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCenterButton", required: false }] }], showSkipBack: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSkipBack", required: false }] }], showVolume: [{ type: i0.Input, args: [{ isSignal: true, alias: "showVolume", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], showCaptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCaptions", required: false }] }], showSettings: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSettings", required: false }] }], showFullscreen: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFullscreen", required: false }] }], seekStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "seekStep", required: false }] }], playbackRates: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRates", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }, { type: i0.Output, args: ["playingChange"] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }, { type: i0.Output, args: ["currentTimeChange"] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }, { type: i0.Output, args: ["volumeChange"] }], muted: [{ type: i0.Input, args: [{ isSignal: true, alias: "muted", required: false }] }, { type: i0.Output, args: ["mutedChange"] }], playbackRate: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRate", required: false }] }, { type: i0.Output, args: ["playbackRateChange"] }], captionsEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsEnabled", required: false }] }, { type: i0.Output, args: ["captionsEnabledChange"] }], timeUpdate: [{ type: i0.Output, args: ["timeUpdate"] }], loadedMetadata: [{ type: i0.Output, args: ["loadedMetadata"] }], seekEnd: [{ type: i0.Output, args: ["seekEnd"] }], playbackEnded: [{ type: i0.Output, args: ["playbackEnded"] }], fullscreenChange: [{ type: i0.Output, args: ["fullscreenChange"] }], playerError: [{ type: i0.Output, args: ["playerError"] }] } });
11550
+
11551
+ /*
11552
+ * Shared data model for the PECB course components
11553
+ * (`pecb-course-content-panel` + `pecb-course-player`).
11554
+ *
11555
+ * Source of truth: "Course Player — Content Redesign" (data.jsx / panel.jsx).
11556
+ */
11557
+ // ─── Formatting helpers ───────────────────────────────────────────────────────
11558
+ /**
11559
+ * Human-readable lesson length: `"9 min"`, `"12 min 4 sec"`, `"1 hr 3 min"`.
11560
+ * Non-finite / negative input renders as `"0 min"`.
11561
+ */
11562
+ function formatCourseDuration(seconds) {
11563
+ if (!Number.isFinite(seconds) || seconds < 0)
11564
+ return '0 min';
11565
+ const total = Math.floor(seconds);
11566
+ const minutes = Math.floor(total / 60);
11567
+ const rest = total % 60;
11568
+ if (minutes >= 60) {
11569
+ const hours = Math.floor(minutes / 60);
11570
+ return `${hours} hr ${minutes % 60} min`;
11571
+ }
11572
+ return `${minutes} min${rest ? ` ${rest} sec` : ''}`;
11573
+ }
11574
+ /**
11575
+ * Clock reading: `m:ss`, or `h:mm:ss` from one hour up.
11576
+ * Non-finite / negative input renders as `0:00`.
11577
+ */
11578
+ function formatCourseTime(seconds) {
11579
+ if (!Number.isFinite(seconds) || seconds < 0)
11580
+ return '0:00';
11581
+ const total = Math.floor(seconds);
11582
+ const h = Math.floor(total / 3600);
11583
+ const m = Math.floor((total % 3600) / 60);
11584
+ const s = total % 60;
11585
+ const mm = h > 0 ? String(m).padStart(2, '0') : String(m);
11586
+ return h > 0 ? `${h}:${mm}:${String(s).padStart(2, '0')}` : `${mm}:${String(s).padStart(2, '0')}`;
11587
+ }
11588
+ // ─── Derivation helpers ───────────────────────────────────────────────────────
11589
+ const PART_MARKER = ' — Part ';
11590
+ /**
11591
+ * Splits `"Base — Part 3"` into `{ base: 'Base', part: 'Part 3' }`.
11592
+ * A title without the marker yields `{ base: title, part: null }`.
11593
+ */
11594
+ function parseLessonPartTitle(title) {
11595
+ const index = title.indexOf(PART_MARKER);
11596
+ if (index === -1)
11597
+ return { base: title, part: null };
11598
+ return { base: title.slice(0, index), part: `Part ${title.slice(index + PART_MARKER.length).trim()}` };
11599
+ }
11600
+ /**
11601
+ * Resolves the status actually rendered for a lesson.
11602
+ *
11603
+ * The active video lesson follows live playback (`playing` while the player is
11604
+ * running, otherwise `started` — unless it is already `completed`), and a
11605
+ * `playing` status on any *other* lesson degrades to `started`, so exactly one
11606
+ * row can ever show the "now playing" treatment. `locked` always wins.
11607
+ */
11608
+ function resolveLessonStatus(lesson, active, livePlaying) {
11609
+ const raw = lesson.status ?? 'available';
11610
+ if (raw === 'locked')
11611
+ return 'locked';
11612
+ if (active && (lesson.type ?? 'video') === 'video') {
11613
+ return livePlaying ? 'playing' : raw === 'completed' ? 'completed' : 'started';
11614
+ }
11615
+ if (!active && raw === 'playing')
11616
+ return 'started';
11617
+ return raw;
11618
+ }
11619
+ /** Counters for one module, computed from already-resolved lesson statuses. */
11620
+ function courseModuleStats(module, resolve = (l) => l.status ?? 'available') {
11621
+ const total = module.lessons.length;
11622
+ let done = 0;
11623
+ let playing = false;
11624
+ let started = false;
11625
+ let durationSeconds = 0;
11626
+ for (const lesson of module.lessons) {
11627
+ const status = resolve(lesson);
11628
+ if (status === 'completed')
11629
+ done++;
11630
+ if (status === 'playing')
11631
+ playing = true;
11632
+ if (status === 'started')
11633
+ started = true;
11634
+ durationSeconds += lesson.durationSeconds ?? 0;
11635
+ }
11636
+ return {
11637
+ total,
11638
+ done,
11639
+ playing,
11640
+ started,
11641
+ durationSeconds,
11642
+ percent: total > 0 ? Math.round((done / total) * 100) : 0,
11643
+ };
11644
+ }
11645
+ /** Counters for a whole course. */
11646
+ function courseProgress(modules, resolve = (l) => l.status ?? 'available') {
11647
+ const lessons = modules.flatMap((m) => m.lessons);
11648
+ const total = lessons.length;
11649
+ let done = 0;
11650
+ let remainingSeconds = 0;
11651
+ for (const lesson of lessons) {
11652
+ const status = resolve(lesson);
11653
+ if (status === 'completed') {
11654
+ done++;
11655
+ }
11656
+ else if ((lesson.type ?? 'video') === 'video') {
11657
+ remainingSeconds += lesson.durationSeconds ?? 0;
11658
+ }
11659
+ }
11660
+ return {
11661
+ total,
11662
+ done,
11663
+ percent: total > 0 ? Math.round((done / total) * 100) : 0,
11664
+ remainingSeconds,
11665
+ };
11666
+ }
11667
+ /** Finds the module a lesson belongs to. */
11668
+ function findCourseModule(modules, lessonId) {
11669
+ if (!lessonId)
11670
+ return undefined;
11671
+ return modules.find((m) => m.lessons.some((l) => l.id === lessonId));
11672
+ }
11673
+ /** Finds a lesson by id across all modules. */
11674
+ function findCourseLesson(modules, lessonId) {
11675
+ if (!lessonId)
11676
+ return undefined;
11677
+ for (const module of modules) {
11678
+ const lesson = module.lessons.find((l) => l.id === lessonId);
11679
+ if (lesson)
11680
+ return lesson;
11681
+ }
11682
+ return undefined;
11683
+ }
11684
+
11685
+ const DEFAULT_COURSE_CONTENT_PANEL_LABELS = {
11686
+ contentTab: 'Content',
11687
+ transcriptTab: 'Transcript',
11688
+ searchLessons: 'Search lessons and quizzes',
11689
+ searchTranscript: 'Search transcript',
11690
+ clearSearch: 'Clear search',
11691
+ quiz: 'Quiz',
11692
+ questions: '{n} questions',
11693
+ passed: 'Passed · {done}/{total}',
11694
+ answered: '{done} of {total} answered',
11695
+ resume: 'Resume',
11696
+ moduleDone: '{done}/{total} done',
11697
+ section: 'Section {n}',
11698
+ noLessons: 'No lessons match “{query}”.',
11699
+ noTranscript: 'No transcript lines match “{query}”.',
11700
+ autoscroll: 'Autoscroll',
11701
+ courseProgress: 'Course progress',
11702
+ courseProgressDetail: '{done} of {total} lessons · {duration} left',
11703
+ certificate: 'Certificate',
11704
+ locked: 'Locked',
11705
+ lessonList: 'Course content',
11706
+ };
11707
+ // Disc geometry per density (design: 30 / 26 px).
11708
+ const DISC_SIZE = { comfortable: 30, compact: 26 };
11709
+ const RING_STROKE = 3;
11710
+ // ─── Component ────────────────────────────────────────────────────────────────
11711
+ /**
11712
+ * PECB course **Content** panel — the sidebar that sits next to the course
11713
+ * video player.
11714
+ *
11715
+ * Reproduces the "Course Player — Content Redesign" panel exactly: the
11716
+ * Content / Transcript tabs, lesson search, the collapsible **Section**
11717
+ * accordion (one section open at a time, auto-opening the section that owns the
11718
+ * active lesson), status discs (completed / now playing / in-progress ring /
11719
+ * not started / locked), quiz rows with badge + score, per-lesson progress bars,
11720
+ * the "Part 1 / Part 2 …" timeline rail for multi-part lessons, an alternative
11721
+ * flat timeline layout, and the searchable transcript with click-to-seek,
11722
+ * live cue highlighting and an autoscroll toggle.
11723
+ *
11724
+ * It is standalone and data-driven — no player dependency — so it can be used
11725
+ * next to `pecb-video-player`, inside `pecb-course-player`, or on its own.
11726
+ *
11727
+ * @example
11728
+ * ```html
11729
+ * <pecb-course-content-panel
11730
+ * [modules]="modules"
11731
+ * [transcript]="cues"
11732
+ * [(activeLessonId)]="lessonId"
11733
+ * [playing]="playing()"
11734
+ * [currentTime]="currentTime()"
11735
+ * (lessonSelect)="openLesson($event)"
11736
+ * (transcriptSeek)="seekTo($event)">
11737
+ * </pecb-course-content-panel>
11738
+ * ```
11739
+ *
11740
+ * Theming — override the CSS custom properties on the host:
11741
+ * ```css
11742
+ * pecb-course-content-panel { --pecb-course-panel-accent: #a11e29; }
11743
+ * ```
11744
+ */
11745
+ class CourseContentPanelComponent {
11746
+ constructor() {
11747
+ this.platformId = inject(PLATFORM_ID);
11748
+ this.injector = inject(Injector);
11749
+ this.isBrowser = isPlatformBrowser(this.platformId);
11750
+ /** Unique id prefix used to wire ARIA relationships. */
11751
+ this.uid = generateUniqueId('pecb-course-panel');
11752
+ this.transcriptScrollRef = viewChild('transcriptScroll', ...(ngDevMode ? [{ debugName: "transcriptScrollRef" }] : []));
11753
+ // ── Data inputs ─────────────────────────────────────────────────────────────
11754
+ /** Course structure rendered by the Content tab. */
11755
+ this.modules = input([], ...(ngDevMode ? [{ debugName: "modules" }] : []));
11756
+ /**
11757
+ * Transcript cues for the active lesson. A `transcript` on the active
11758
+ * `CourseLesson` takes precedence over this input.
11759
+ */
11760
+ this.transcript = input([], ...(ngDevMode ? [{ debugName: "transcript" }] : []));
11761
+ // ── State (two-way) ─────────────────────────────────────────────────────────
11762
+ /** Currently selected lesson. */
11763
+ this.activeLessonId = model(null, ...(ngDevMode ? [{ debugName: "activeLessonId" }] : []));
11764
+ /** Visible tab. */
11765
+ this.tab = model('content', ...(ngDevMode ? [{ debugName: "tab" }] : []));
11766
+ /**
11767
+ * Id of the expanded section. `null` collapses all of them.
11768
+ * Left `undefined` the panel picks one itself (the active lesson's section,
11769
+ * otherwise the first one).
11770
+ */
11771
+ this.openModuleId = model(undefined, ...(ngDevMode ? [{ debugName: "openModuleId" }] : []));
11772
+ /** Whether the transcript follows playback. */
11773
+ this.autoscroll = model(true, ...(ngDevMode ? [{ debugName: "autoscroll" }] : []));
11774
+ /** Lesson search text (Content tab). */
11775
+ this.query = model('', ...(ngDevMode ? [{ debugName: "query" }] : []));
11776
+ /** Transcript search text. */
11777
+ this.transcriptQuery = model('', ...(ngDevMode ? [{ debugName: "transcriptQuery" }] : []));
11778
+ // ── Live playback state ─────────────────────────────────────────────────────
11779
+ /** Whether the player is currently playing — drives the "now playing" row. */
11780
+ this.playing = input(false, ...(ngDevMode ? [{ debugName: "playing", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11781
+ /**
11782
+ * Live playhead position in seconds. Used to highlight the transcript cue and
11783
+ * to draw live progress on the active lesson row; falls back to each lesson's
11784
+ * `resumeSeconds` when omitted.
11785
+ */
11786
+ this.currentTime = input(undefined, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
11787
+ // ── Appearance ──────────────────────────────────────────────────────────────
11788
+ /** @default 'grouped' */
11789
+ this.layout = input('grouped', ...(ngDevMode ? [{ debugName: "layout" }] : []));
11790
+ /** @default 'comfortable' */
11791
+ this.density = input('comfortable', ...(ngDevMode ? [{ debugName: "density" }] : []));
11792
+ /** Colour of the "now playing" treatment. @default 'charcoal' */
11793
+ this.accent = input('charcoal', ...(ngDevMode ? [{ debugName: "accent" }] : []));
11794
+ /** Show the Content / Transcript tab strip. @default true */
11795
+ this.showTabs = input(true, ...(ngDevMode ? [{ debugName: "showTabs", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11796
+ /** Show the lesson search box. @default true */
11797
+ this.showSearch = input(true, ...(ngDevMode ? [{ debugName: "showSearch", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11798
+ /** Show the transcript search box. @default true */
11799
+ this.showTranscriptSearch = input(true, ...(ngDevMode ? [{ debugName: "showTranscriptSearch", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11800
+ /** Show the autoscroll footer on the Transcript tab. @default true */
11801
+ this.showAutoscroll = input(true, ...(ngDevMode ? [{ debugName: "showAutoscroll", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11802
+ /** Show the circular course-progress header above the list. @default false */
11803
+ this.showProgress = input(false, ...(ngDevMode ? [{ debugName: "showProgress", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11804
+ /** Show the "Certificate" pill inside the progress header. @default true */
11805
+ this.showCertificate = input(true, ...(ngDevMode ? [{ debugName: "showCertificate", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11806
+ /**
11807
+ * Render a module that holds exactly one lesson as a single direct-play row
11808
+ * instead of a collapsible section. @default false
11809
+ */
11810
+ this.soloSingleLessonModules = input(false, ...(ngDevMode ? [{ debugName: "soloSingleLessonModules", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11811
+ /** Partial override of the user-visible strings (i18n). */
11812
+ this.labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : []));
11813
+ // ── Outputs ─────────────────────────────────────────────────────────────────
11814
+ /** A lesson row was activated. */
11815
+ this.lessonSelect = output();
11816
+ /** A transcript cue was clicked — seek the player to this position (seconds). */
11817
+ this.transcriptSeek = output();
11818
+ /** A section was expanded or collapsed. */
11819
+ this.moduleToggle = output();
11820
+ /** The "Certificate" pill was clicked. */
11821
+ this.certificateClick = output();
11822
+ // ── Labels ──────────────────────────────────────────────────────────────────
11823
+ this.text = computed(() => ({
11824
+ ...DEFAULT_COURSE_CONTENT_PANEL_LABELS,
11825
+ ...this.labels(),
11826
+ }), ...(ngDevMode ? [{ debugName: "text" }] : []));
11827
+ // ── Theming ─────────────────────────────────────────────────────────────────
11828
+ this.hostClasses = computed(() => [
11829
+ 'pecb-course-panel',
11830
+ `pecb-course-panel--${this.density()}`,
11831
+ `pecb-course-panel--${this.layout()}`,
11832
+ `pecb-course-panel--accent-${this.accent()}`,
11833
+ ].join(' '), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
11834
+ // ── Derived data ────────────────────────────────────────────────────────────
11835
+ this.compact = computed(() => this.density() === 'compact', ...(ngDevMode ? [{ debugName: "compact" }] : []));
11836
+ /** Resolved status of every lesson, keyed by lesson id. */
11837
+ this.statusById = computed(() => {
11838
+ const activeId = this.activeLessonId();
11839
+ const livePlaying = this.playing();
11840
+ const map = new Map();
11841
+ for (const module of this.modules()) {
11842
+ for (const lesson of module.lessons) {
11843
+ map.set(lesson.id, resolveLessonStatus(lesson, lesson.id === activeId, livePlaying));
11844
+ }
11845
+ }
11846
+ return map;
11847
+ }, ...(ngDevMode ? [{ debugName: "statusById" }] : []));
11848
+ this.resolveStatus = computed(() => {
11849
+ const map = this.statusById();
11850
+ return (lesson) => map.get(lesson.id) ?? (lesson.status ?? 'available');
11851
+ }, ...(ngDevMode ? [{ debugName: "resolveStatus" }] : []));
11852
+ /** Modules narrowed by the search box (design: match lesson titles, else module title). */
11853
+ this.filteredModules = computed(() => {
11854
+ const term = this.query().trim().toLowerCase();
11855
+ const modules = this.modules();
11856
+ if (!term)
11857
+ return modules;
11858
+ return modules
11859
+ .map((module) => ({
11860
+ ...module,
11861
+ lessons: module.lessons.filter((lesson) => lesson.title.toLowerCase().includes(term)),
11862
+ }))
11863
+ .filter((module) => module.lessons.length > 0 || module.title.toLowerCase().includes(term));
11864
+ }, ...(ngDevMode ? [{ debugName: "filteredModules" }] : []));
11865
+ this.moduleViews = computed(() => {
11866
+ const all = this.modules();
11867
+ const activeId = this.activeLessonId();
11868
+ const solo = this.soloSingleLessonModules();
11869
+ const text = this.text();
11870
+ const livePlaying = this.playing();
11871
+ return this.filteredModules().map((module) => {
11872
+ const index = all.findIndex((m) => m.id === module.id);
11873
+ const active = module.lessons.some((lesson) => lesson.id === activeId);
11874
+ const isSolo = solo && module.lessons.length === 1;
11875
+ // Section counters come from the stored statuses (design behaviour), with
11876
+ // live playback folded in so the badge reacts while the video runs.
11877
+ const raw = courseModuleStats(module);
11878
+ const stats = {
11879
+ ...raw,
11880
+ playing: raw.playing || (active && livePlaying),
11881
+ };
11882
+ return {
11883
+ module,
11884
+ id: module.id,
11885
+ number: (index === -1 ? all.length : index) + 1,
11886
+ title: module.title,
11887
+ stats,
11888
+ statsLabel: format(text.moduleDone, { done: stats.done, total: stats.total }),
11889
+ durationLabel: formatCourseDuration(stats.durationSeconds),
11890
+ allDone: stats.total > 0 && stats.done === stats.total,
11891
+ active,
11892
+ runs: isSolo ? [] : this.buildRuns(module.lessons),
11893
+ solo: isSolo ? this.buildLessonView(module.lessons[0], { titleOverride: module.title }) : null,
11894
+ };
11895
+ });
11896
+ }, ...(ngDevMode ? [{ debugName: "moduleViews" }] : []));
11897
+ this.timelineRows = computed(() => {
11898
+ const all = this.modules();
11899
+ const activeId = this.activeLessonId();
11900
+ const livePlaying = this.playing();
11901
+ const rows = [];
11902
+ for (const module of this.filteredModules()) {
11903
+ const index = all.findIndex((m) => m.id === module.id);
11904
+ const rawStats = courseModuleStats(module);
11905
+ rows.push({
11906
+ key: `h-${module.id}`,
11907
+ header: {
11908
+ number: (index === -1 ? all.length : index) + 1,
11909
+ title: module.title,
11910
+ stats: {
11911
+ ...rawStats,
11912
+ playing: rawStats.playing ||
11913
+ (livePlaying && module.lessons.some((l) => l.id === activeId)),
11914
+ },
11915
+ },
11916
+ lesson: null,
11917
+ });
11918
+ module.lessons.forEach((lesson, i) => {
11919
+ rows.push({
11920
+ key: lesson.id,
11921
+ header: null,
11922
+ lesson: this.buildLessonView(lesson, { connector: i < module.lessons.length - 1 }),
11923
+ });
11924
+ });
11925
+ }
11926
+ return rows;
11927
+ }, ...(ngDevMode ? [{ debugName: "timelineRows" }] : []));
11928
+ this.isEmpty = computed(() => this.filteredModules().length === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
11929
+ // ── Progress header ─────────────────────────────────────────────────────────
11930
+ this.progress = computed(() => courseProgress(this.modules()), ...(ngDevMode ? [{ debugName: "progress" }] : []));
11931
+ /** Geometry of the 56 px progress ring (design: r = 22, stroke = 5). */
11932
+ this.progressRing = computed(() => {
11933
+ const radius = 22;
11934
+ const circumference = 2 * Math.PI * radius;
11935
+ return {
11936
+ radius,
11937
+ circumference,
11938
+ offset: circumference * (1 - this.progress().percent / 100),
11939
+ };
11940
+ }, ...(ngDevMode ? [{ debugName: "progressRing" }] : []));
11941
+ this.progressDetail = computed(() => {
11942
+ const stats = this.progress();
11943
+ return format(this.text().courseProgressDetail, {
11944
+ done: stats.done,
11945
+ total: stats.total,
11946
+ duration: formatCourseDuration(stats.remainingSeconds),
11947
+ });
11948
+ }, ...(ngDevMode ? [{ debugName: "progressDetail" }] : []));
11949
+ // ── Transcript ──────────────────────────────────────────────────────────────
11950
+ /** Cues of the active lesson, falling back to the panel-level `transcript`. */
11951
+ this.activeCues = computed(() => {
11952
+ const activeId = this.activeLessonId();
11953
+ for (const module of this.modules()) {
11954
+ const lesson = module.lessons.find((l) => l.id === activeId);
11955
+ if (lesson?.transcript?.length)
11956
+ return lesson.transcript;
11957
+ }
11958
+ return this.transcript();
11959
+ }, ...(ngDevMode ? [{ debugName: "activeCues" }] : []));
11960
+ /** Index of the cue covering the playhead, or `-1`. */
11961
+ this.activeCueIndex = computed(() => {
11962
+ const cues = this.activeCues();
11963
+ const at = this.currentTime();
11964
+ if (at === undefined || cues.length === 0)
11965
+ return -1;
11966
+ let found = -1;
11967
+ for (let i = 0; i < cues.length; i++) {
11968
+ const next = cues[i + 1];
11969
+ if (at >= cues[i].time && (!next || at < next.time)) {
11970
+ found = i;
11971
+ break;
11972
+ }
11973
+ }
11974
+ return found;
11975
+ }, ...(ngDevMode ? [{ debugName: "activeCueIndex" }] : []));
11976
+ this.transcriptRows = computed(() => {
11977
+ const cues = this.activeCues();
11978
+ const term = this.transcriptQuery().trim();
11979
+ const lower = term.toLowerCase();
11980
+ const activeIndex = this.activeCueIndex();
11981
+ const rows = [];
11982
+ cues.forEach((cue, index) => {
11983
+ const matchIndex = lower ? cue.text.toLowerCase().indexOf(lower) : -1;
11984
+ if (lower && matchIndex === -1)
11985
+ return;
11986
+ rows.push({
11987
+ key: `${index}-${cue.time}`,
11988
+ cue,
11989
+ timeLabel: formatCourseTime(cue.time),
11990
+ // While searching, the design drops the "current cue" highlight.
11991
+ current: !lower && index === activeIndex,
11992
+ before: matchIndex === -1 ? cue.text : cue.text.slice(0, matchIndex),
11993
+ match: matchIndex === -1 ? '' : cue.text.slice(matchIndex, matchIndex + term.length),
11994
+ after: matchIndex === -1 ? '' : cue.text.slice(matchIndex + term.length),
11995
+ });
11996
+ });
11997
+ return rows;
11998
+ }, ...(ngDevMode ? [{ debugName: "transcriptRows" }] : []));
11999
+ this.transcriptEmpty = computed(() => this.transcriptRows().length === 0, ...(ngDevMode ? [{ debugName: "transcriptEmpty" }] : []));
12000
+ // ── Messages ────────────────────────────────────────────────────────────────
12001
+ this.noLessonsLabel = computed(() => format(this.text().noLessons, { query: this.query().trim() }), ...(ngDevMode ? [{ debugName: "noLessonsLabel" }] : []));
12002
+ this.noTranscriptLabel = computed(() => format(this.text().noTranscript, { query: this.transcriptQuery().trim() }), ...(ngDevMode ? [{ debugName: "noTranscriptLabel" }] : []));
12003
+ // Keep the section holding the active lesson expanded. Tracks `activeLessonId`
12004
+ // only — so collapsing the current section by hand stays collapsed until the
12005
+ // selection actually changes (design behaviour).
12006
+ effect(() => {
12007
+ const activeId = this.activeLessonId();
12008
+ untracked(() => {
12009
+ if (!activeId)
12010
+ return;
12011
+ const module = findCourseModule(this.modules(), activeId);
12012
+ if (module)
12013
+ this.openModuleId.set(module.id);
12014
+ });
12015
+ });
12016
+ // First time modules are available and nothing has been decided yet, expand
12017
+ // the active lesson's section, else the first one.
12018
+ let autoOpened = false;
12019
+ effect(() => {
12020
+ const modules = this.modules();
12021
+ untracked(() => {
12022
+ if (autoOpened || modules.length === 0)
12023
+ return;
12024
+ autoOpened = true;
12025
+ if (this.openModuleId() !== undefined)
12026
+ return;
12027
+ const module = findCourseModule(modules, this.activeLessonId());
12028
+ this.openModuleId.set(module?.id ?? modules[0].id);
12029
+ });
12030
+ });
12031
+ // Follow the playhead in the transcript.
12032
+ effect(() => {
12033
+ const index = this.activeCueIndex();
12034
+ const auto = this.autoscroll();
12035
+ const tab = this.tab();
12036
+ const searching = this.transcriptQuery().trim().length > 0;
12037
+ untracked(() => {
12038
+ if (!this.isBrowser || !auto || searching || tab !== 'transcript' || index < 0)
12039
+ return;
12040
+ afterNextRender(() => this.scrollActiveCueIntoView(), { injector: this.injector });
12041
+ });
12042
+ });
12043
+ }
12044
+ // ── Template helpers ────────────────────────────────────────────────────────
12045
+ /** `"Section 3"` — the `{n}` placeholder resolved against the label set. */
12046
+ sectionLabel(index) {
12047
+ return format(this.text().section, { n: index });
12048
+ }
12049
+ /** Whether a section is expanded. */
12050
+ isModuleOpen(moduleId) {
12051
+ return this.openModuleId() === moduleId;
12052
+ }
12053
+ /** Expand a section, collapsing the one that was open (single-open accordion). */
12054
+ toggleModule(view) {
12055
+ const open = !this.isModuleOpen(view.id);
12056
+ this.openModuleId.set(open ? view.id : null);
12057
+ this.moduleToggle.emit({ module: view.module, open });
12058
+ }
12059
+ /** Roving keyboard support between section headers (WAI-ARIA accordion). */
12060
+ onModuleHeaderKeydown(event) {
12061
+ const key = event.key;
12062
+ if (key !== 'ArrowDown' && key !== 'ArrowUp' && key !== 'Home' && key !== 'End')
12063
+ return;
12064
+ const target = event.target;
12065
+ const root = target.closest('.pecb-course-panel__list');
12066
+ if (!root)
12067
+ return;
12068
+ const headers = Array.from(root.querySelectorAll('.pecb-course-panel__section-head'));
12069
+ if (headers.length === 0)
12070
+ return;
12071
+ const index = headers.indexOf(target);
12072
+ if (index === -1)
12073
+ return;
12074
+ let next;
12075
+ switch (key) {
12076
+ case 'ArrowDown':
12077
+ next = (index + 1) % headers.length;
12078
+ break;
12079
+ case 'ArrowUp':
12080
+ next = (index - 1 + headers.length) % headers.length;
12081
+ break;
12082
+ case 'Home':
12083
+ next = 0;
12084
+ break;
12085
+ default:
12086
+ next = headers.length - 1;
12087
+ }
12088
+ event.preventDefault();
12089
+ headers[next].focus();
12090
+ }
12091
+ /** Activate a lesson (ignored when locked). */
12092
+ selectLesson(lesson) {
12093
+ if ((lesson.status ?? 'available') === 'locked')
12094
+ return;
12095
+ this.activeLessonId.set(lesson.id);
12096
+ this.lessonSelect.emit(lesson);
12097
+ }
12098
+ /** Seek the player to a transcript cue. */
12099
+ seekToCue(cue) {
12100
+ this.transcriptSeek.emit(cue.time);
12101
+ }
12102
+ setTab(tab) {
12103
+ this.tab.set(tab);
12104
+ }
12105
+ onTabKeydown(event) {
12106
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
12107
+ return;
12108
+ event.preventDefault();
12109
+ this.setTab(this.tab() === 'content' ? 'transcript' : 'content');
12110
+ const next = event.target
12111
+ .closest('[role="tablist"]')
12112
+ ?.querySelector('[aria-selected="false"]');
12113
+ next?.focus();
12114
+ }
12115
+ onQueryInput(event) {
12116
+ this.query.set(event.target.value);
12117
+ }
12118
+ onTranscriptQueryInput(event) {
12119
+ this.transcriptQuery.set(event.target.value);
12120
+ }
12121
+ clearTranscriptQuery() {
12122
+ this.transcriptQuery.set('');
12123
+ }
12124
+ toggleAutoscroll() {
12125
+ this.autoscroll.update((value) => !value);
12126
+ }
12127
+ onAutoscrollChange(event) {
12128
+ this.autoscroll.set(event.target.checked);
12129
+ }
12130
+ // ── Internals ───────────────────────────────────────────────────────────────
12131
+ /**
12132
+ * Splits a module's lessons into runs of consecutive lessons sharing a base
12133
+ * title. A run of two or more where at least one carries a `— Part n` suffix
12134
+ * renders as a sub-heading plus a connected rail (design behaviour).
12135
+ */
12136
+ buildRuns(lessons) {
12137
+ const runs = [];
12138
+ let i = 0;
12139
+ while (i < lessons.length) {
12140
+ const base = parseLessonPartTitle(lessons[i].title).base;
12141
+ const run = [];
12142
+ let j = i;
12143
+ while (j < lessons.length && parseLessonPartTitle(lessons[j].title).base === base) {
12144
+ run.push(lessons[j]);
12145
+ j++;
12146
+ }
12147
+ const hasParts = run.some((lesson) => parseLessonPartTitle(lesson.title).part !== null);
12148
+ if (hasParts && run.length > 1) {
12149
+ runs.push({
12150
+ key: `parts-${run[0].id}`,
12151
+ parts: true,
12152
+ base,
12153
+ lessons: run.map((lesson) => {
12154
+ const { part } = parseLessonPartTitle(lesson.title);
12155
+ // Inside a parts rail the quiz shows only its badge (design).
12156
+ const titleOverride = (lesson.type ?? 'video') === 'quiz' ? null : part ?? lesson.title;
12157
+ return this.buildLessonView(lesson, { titleOverride });
12158
+ }),
12159
+ });
12160
+ }
12161
+ else {
12162
+ runs.push({
12163
+ key: `plain-${run[0].id}`,
12164
+ parts: false,
12165
+ base,
12166
+ lessons: run.map((lesson) => this.buildLessonView(lesson)),
12167
+ });
12168
+ }
12169
+ i = j;
12170
+ }
12171
+ return runs;
12172
+ }
12173
+ buildLessonView(lesson, options = {}) {
12174
+ const text = this.text();
12175
+ const compact = this.compact();
12176
+ const active = lesson.id === this.activeLessonId();
12177
+ const status = this.resolveStatus()(lesson);
12178
+ const isQuiz = (lesson.type ?? 'video') === 'quiz';
12179
+ const isPlaying = status === 'playing';
12180
+ const isStarted = status === 'started';
12181
+ const isCompleted = status === 'completed';
12182
+ const isLocked = status === 'locked';
12183
+ const duration = lesson.durationSeconds ?? 0;
12184
+ const questions = lesson.questionCount ?? 0;
12185
+ const answered = lesson.answeredCount ?? 0;
12186
+ // The active lesson follows the live playhead; everything else uses its
12187
+ // stored resume position.
12188
+ const live = this.currentTime();
12189
+ const at = active && live !== undefined ? live : lesson.resumeSeconds ?? 0;
12190
+ const fraction = isQuiz
12191
+ ? questions
12192
+ ? Math.min(1, answered / questions)
12193
+ : 0.4
12194
+ : duration
12195
+ ? Math.min(1, at / duration)
12196
+ : 0.4;
12197
+ const discSize = compact ? DISC_SIZE.compact : DISC_SIZE.comfortable;
12198
+ const ringRadius = discSize / 2 - RING_STROKE / 2;
12199
+ const ringCircumference = 2 * Math.PI * ringRadius;
12200
+ const showBar = (isPlaying || isStarted) && (isQuiz ? questions > 0 : duration > 0);
12201
+ const barPercent = isQuiz
12202
+ ? questions
12203
+ ? (answered / questions) * 100
12204
+ : 0
12205
+ : duration
12206
+ ? Math.min(100, (at / duration) * 100)
12207
+ : 0;
12208
+ let quizResultLabel = null;
12209
+ if (isQuiz && isCompleted) {
12210
+ quizResultLabel = format(text.passed, { done: lesson.correctCount ?? 0, total: questions });
12211
+ }
12212
+ else if (isQuiz && isStarted) {
12213
+ quizResultLabel = format(text.answered, { done: answered, total: questions });
12214
+ }
12215
+ const timeLabel = !isQuiz && (isPlaying || isStarted) && duration
12216
+ ? `${isStarted ? `${text.resume} ` : ''}${formatCourseTime(at)} / ${formatCourseTime(duration)}`
12217
+ : null;
12218
+ const displayTitle = options.titleOverride !== undefined ? options.titleOverride : lesson.title;
12219
+ return {
12220
+ lesson,
12221
+ id: lesson.id,
12222
+ active,
12223
+ status,
12224
+ isQuiz,
12225
+ isPlaying,
12226
+ isStarted,
12227
+ isCompleted,
12228
+ isLocked,
12229
+ displayTitle,
12230
+ ariaLabel: isLocked ? `${lesson.title} — ${text.locked}` : lesson.title,
12231
+ durationLabel: formatCourseDuration(duration),
12232
+ questionsLabel: format(text.questions, { n: questions }),
12233
+ quizResultLabel,
12234
+ timeLabel,
12235
+ showBar,
12236
+ barPercent,
12237
+ discSize,
12238
+ discCenter: discSize / 2,
12239
+ ringRadius,
12240
+ ringCircumference,
12241
+ ringOffset: ringCircumference * (1 - fraction),
12242
+ ringViewBox: `0 0 ${discSize} ${discSize}`,
12243
+ checkSize: compact ? 14 : 16,
12244
+ pauseSize: compact ? 11 : 12,
12245
+ // The in-progress ring leaves a touch less room than the plain outline.
12246
+ playIconSize: isStarted ? (compact ? 9 : 10) : compact ? 10 : 11,
12247
+ quizIconSize: isStarted ? (compact ? 13 : 14) : compact ? 14 : 15,
12248
+ connector: options.connector ?? false,
12249
+ connectorDone: isCompleted,
12250
+ };
12251
+ }
12252
+ scrollActiveCueIntoView() {
12253
+ const container = this.transcriptScrollRef()?.nativeElement;
12254
+ if (!container)
12255
+ return;
12256
+ const row = container.querySelector('[data-current="true"]');
12257
+ if (!row)
12258
+ return;
12259
+ const rowRect = row.getBoundingClientRect();
12260
+ const boxRect = container.getBoundingClientRect();
12261
+ const top = container.scrollTop + (rowRect.top - boxRect.top) - (boxRect.height / 2 - rowRect.height / 2);
12262
+ const reduceMotion = typeof window !== 'undefined' &&
12263
+ typeof window.matchMedia === 'function' &&
12264
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
12265
+ container.scrollTo({ top, behavior: reduceMotion ? 'auto' : 'smooth' });
12266
+ }
12267
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CourseContentPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
12268
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: CourseContentPanelComponent, isStandalone: true, selector: "pecb-course-content-panel", inputs: { modules: { classPropertyName: "modules", publicName: "modules", isSignal: true, isRequired: false, transformFunction: null }, transcript: { classPropertyName: "transcript", publicName: "transcript", isSignal: true, isRequired: false, transformFunction: null }, activeLessonId: { classPropertyName: "activeLessonId", publicName: "activeLessonId", isSignal: true, isRequired: false, transformFunction: null }, tab: { classPropertyName: "tab", publicName: "tab", isSignal: true, isRequired: false, transformFunction: null }, openModuleId: { classPropertyName: "openModuleId", publicName: "openModuleId", isSignal: true, isRequired: false, transformFunction: null }, autoscroll: { classPropertyName: "autoscroll", publicName: "autoscroll", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, transcriptQuery: { classPropertyName: "transcriptQuery", publicName: "transcriptQuery", isSignal: true, isRequired: false, transformFunction: null }, playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, showTabs: { classPropertyName: "showTabs", publicName: "showTabs", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, showTranscriptSearch: { classPropertyName: "showTranscriptSearch", publicName: "showTranscriptSearch", isSignal: true, isRequired: false, transformFunction: null }, showAutoscroll: { classPropertyName: "showAutoscroll", publicName: "showAutoscroll", isSignal: true, isRequired: false, transformFunction: null }, showProgress: { classPropertyName: "showProgress", publicName: "showProgress", isSignal: true, isRequired: false, transformFunction: null }, showCertificate: { classPropertyName: "showCertificate", publicName: "showCertificate", isSignal: true, isRequired: false, transformFunction: null }, soloSingleLessonModules: { classPropertyName: "soloSingleLessonModules", publicName: "soloSingleLessonModules", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeLessonId: "activeLessonIdChange", tab: "tabChange", openModuleId: "openModuleIdChange", autoscroll: "autoscrollChange", query: "queryChange", transcriptQuery: "transcriptQueryChange", lessonSelect: "lessonSelect", transcriptSeek: "transcriptSeek", moduleToggle: "moduleToggle", certificateClick: "certificateClick" }, host: { classAttribute: "pecb-course-panel-host" }, viewQueries: [{ propertyName: "transcriptScrollRef", first: true, predicate: ["transcriptScroll"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Lesson row \u2014 one <button> per lesson, shared by the grouped, parts-rail,\n timeline and solo renderings.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<ng-template #lessonRowTpl let-vm let-solo=\"solo\">\n <button\n type=\"button\"\n class=\"pecb-course-panel__row\"\n [class.pecb-course-panel__row--active]=\"vm.active\"\n [class.pecb-course-panel__row--quiz]=\"vm.isQuiz\"\n [class.pecb-course-panel__row--playing]=\"vm.isPlaying\"\n [class.pecb-course-panel__row--started]=\"vm.isStarted\"\n [class.pecb-course-panel__row--completed]=\"vm.isCompleted\"\n [class.pecb-course-panel__row--locked]=\"vm.isLocked\"\n [class.pecb-course-panel__row--solo]=\"!!solo\"\n [disabled]=\"vm.isLocked\"\n [attr.aria-current]=\"vm.active ? 'true' : null\"\n [attr.aria-label]=\"vm.ariaLabel\"\n (click)=\"selectLesson(vm.lesson)\"\n >\n @if (vm.active && vm.isPlaying) {\n <span class=\"pecb-course-panel__row-marker\" aria-hidden=\"true\"></span>\n }\n\n <!-- Status disc (+ optional timeline connector) -->\n <span class=\"pecb-course-panel__disc-col\">\n <span\n class=\"pecb-course-panel__disc\"\n [class.pecb-course-panel__disc--completed]=\"vm.isCompleted\"\n [class.pecb-course-panel__disc--playing]=\"vm.isPlaying\"\n [class.pecb-course-panel__disc--started]=\"vm.isStarted\"\n [class.pecb-course-panel__disc--locked]=\"vm.isLocked\"\n [class.pecb-course-panel__disc--plain]=\"!vm.isCompleted && !vm.isPlaying && !vm.isStarted\"\n [style.width.px]=\"vm.discSize\"\n [style.height.px]=\"vm.discSize\"\n aria-hidden=\"true\"\n >\n @if (vm.isCompleted) {\n <svg [attr.width]=\"vm.checkSize\" [attr.height]=\"vm.checkSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n } @else if (vm.isPlaying) {\n <svg [attr.width]=\"vm.pauseSize\" [attr.height]=\"vm.pauseSize\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else if (vm.isStarted) {\n <svg class=\"pecb-course-panel__ring\" [attr.width]=\"vm.discSize\" [attr.height]=\"vm.discSize\" [attr.viewBox]=\"vm.ringViewBox\">\n <circle\n class=\"pecb-course-panel__ring-track\"\n [attr.cx]=\"vm.discCenter\" [attr.cy]=\"vm.discCenter\" [attr.r]=\"vm.ringRadius\"\n fill=\"none\" stroke-width=\"3\" />\n <circle\n class=\"pecb-course-panel__ring-value\"\n [attr.cx]=\"vm.discCenter\" [attr.cy]=\"vm.discCenter\" [attr.r]=\"vm.ringRadius\"\n fill=\"none\" stroke-width=\"3\" stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"vm.ringCircumference\"\n [attr.stroke-dashoffset]=\"vm.ringOffset\" />\n </svg>\n <span class=\"pecb-course-panel__disc-glyph\">\n @if (vm.isQuiz) {\n <svg [attr.width]=\"vm.quizIconSize\" [attr.height]=\"vm.quizIconSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M9.5 9.2a2.5 2.5 0 1 1 3.4 2.3c-.6.3-.9.7-.9 1.4v.4\" />\n <circle cx=\"12\" cy=\"16.3\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n } @else {\n <svg [attr.width]=\"vm.playIconSize\" [attr.height]=\"vm.playIconSize\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </span>\n } @else if (vm.isLocked) {\n <svg [attr.width]=\"vm.checkSize\" [attr.height]=\"vm.checkSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"5\" y=\"11\" width=\"14\" height=\"9\" rx=\"2\" />\n <path d=\"M8 11V8a4 4 0 0 1 8 0v3\" />\n </svg>\n } @else if (vm.isQuiz) {\n <svg [attr.width]=\"vm.quizIconSize\" [attr.height]=\"vm.quizIconSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M9.5 9.2a2.5 2.5 0 1 1 3.4 2.3c-.6.3-.9.7-.9 1.4v.4\" />\n <circle cx=\"12\" cy=\"16.3\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n } @else {\n <svg [attr.width]=\"vm.playIconSize\" [attr.height]=\"vm.playIconSize\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </span>\n\n @if (vm.connector) {\n <span\n class=\"pecb-course-panel__connector\"\n [class.pecb-course-panel__connector--done]=\"vm.connectorDone\"\n aria-hidden=\"true\"\n ></span>\n }\n </span>\n\n <!-- Body -->\n <span class=\"pecb-course-panel__row-body\">\n @if (vm.isQuiz) {\n <span class=\"pecb-course-panel__quiz-badge-line\">\n <span class=\"pecb-course-panel__quiz-badge\">{{ text().quiz }}</span>\n </span>\n }\n\n @if (vm.displayTitle) {\n <span class=\"pecb-course-panel__row-title\">{{ vm.displayTitle }}</span>\n }\n\n <span class=\"pecb-course-panel__row-meta\">\n <span class=\"pecb-course-panel__meta-primary\">\n @if (vm.isQuiz) {\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M9.5 9.2a2.5 2.5 0 1 1 3.4 2.3c-.6.3-.9.7-.9 1.4v.4\" />\n <circle cx=\"12\" cy=\"16.3\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n {{ vm.questionsLabel }}\n } @else {\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <polyline points=\"12 7 12 12 15.5 14\" />\n </svg>\n {{ vm.durationLabel }}\n }\n </span>\n\n @if (vm.quizResultLabel) {\n <span\n class=\"pecb-course-panel__meta-result\"\n [class.pecb-course-panel__meta-result--passed]=\"vm.isCompleted\"\n >\n @if (vm.isCompleted) {\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n {{ vm.quizResultLabel }}\n </span>\n }\n\n @if (vm.timeLabel) {\n <span class=\"pecb-course-panel__meta-time\">\n @if (vm.isStarted) {\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n {{ vm.timeLabel }}\n </span>\n }\n </span>\n\n @if (vm.showBar) {\n <span class=\"pecb-course-panel__bar\" aria-hidden=\"true\">\n <span class=\"pecb-course-panel__bar-fill\" [style.width.%]=\"vm.barPercent\"></span>\n </span>\n }\n </span>\n\n @if (solo) {\n <span class=\"pecb-course-panel__solo-play\" aria-hidden=\"true\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n </span>\n }\n </button>\n</ng-template>\n\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Panel\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<div [class]=\"hostClasses()\">\n @if (showTabs()) {\n <div class=\"pecb-course-panel__tabs\" role=\"tablist\" [attr.aria-label]=\"text().lessonList\">\n <button\n type=\"button\"\n role=\"tab\"\n class=\"pecb-course-panel__tab\"\n [class.pecb-course-panel__tab--active]=\"tab() === 'content'\"\n [id]=\"uid + '-tab-content'\"\n [attr.aria-selected]=\"tab() === 'content'\"\n [attr.aria-controls]=\"uid + '-panel-content'\"\n [attr.tabindex]=\"tab() === 'content' ? 0 : -1\"\n (click)=\"setTab('content')\"\n (keydown)=\"onTabKeydown($event)\"\n >\n {{ text().contentTab }}\n @if (tab() === 'content') {\n <span class=\"pecb-course-panel__tab-indicator\" aria-hidden=\"true\"></span>\n }\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n class=\"pecb-course-panel__tab\"\n [class.pecb-course-panel__tab--active]=\"tab() === 'transcript'\"\n [id]=\"uid + '-tab-transcript'\"\n [attr.aria-selected]=\"tab() === 'transcript'\"\n [attr.aria-controls]=\"uid + '-panel-transcript'\"\n [attr.tabindex]=\"tab() === 'transcript' ? 0 : -1\"\n (click)=\"setTab('transcript')\"\n (keydown)=\"onTabKeydown($event)\"\n >\n {{ text().transcriptTab }}\n @if (tab() === 'transcript') {\n <span class=\"pecb-course-panel__tab-indicator\" aria-hidden=\"true\"></span>\n }\n </button>\n </div>\n }\n\n <!-- \u2500\u2500 Content tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (tab() === 'content') {\n <div\n class=\"pecb-course-panel__body\"\n role=\"tabpanel\"\n [id]=\"uid + '-panel-content'\"\n [attr.aria-labelledby]=\"showTabs() ? uid + '-tab-content' : null\"\n >\n @if (showProgress()) {\n <div class=\"pecb-course-panel__progress\">\n <span class=\"pecb-course-panel__progress-ring\">\n <svg width=\"56\" height=\"56\" viewBox=\"0 0 56 56\" aria-hidden=\"true\">\n <circle class=\"pecb-course-panel__progress-track\" cx=\"28\" cy=\"28\" [attr.r]=\"progressRing().radius\" fill=\"none\" stroke-width=\"5\" />\n <circle\n class=\"pecb-course-panel__progress-value\"\n cx=\"28\" cy=\"28\" [attr.r]=\"progressRing().radius\" fill=\"none\" stroke-width=\"5\" stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"progressRing().circumference\"\n [attr.stroke-dashoffset]=\"progressRing().offset\" />\n </svg>\n <span class=\"pecb-course-panel__progress-percent\">{{ progress().percent }}%</span>\n </span>\n <span class=\"pecb-course-panel__progress-text\">\n <span class=\"pecb-course-panel__progress-title\">{{ text().courseProgress }}</span>\n <span class=\"pecb-course-panel__progress-detail\">{{ progressDetail() }}</span>\n </span>\n @if (showCertificate()) {\n <button type=\"button\" class=\"pecb-course-panel__certificate\" (click)=\"certificateClick.emit()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"8\" r=\"5\" />\n <path d=\"M8.5 12.5 7 21l5-2.5L17 21l-1.5-8.5\" />\n </svg>\n {{ text().certificate }}\n </button>\n }\n </div>\n }\n\n @if (showSearch()) {\n <div class=\"pecb-course-panel__search\">\n <div class=\"pecb-course-panel__search-box\">\n <span class=\"pecb-course-panel__search-icon\" aria-hidden=\"true\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.2-3.2\" />\n </svg>\n </span>\n <input\n type=\"search\"\n class=\"pecb-course-panel__search-input\"\n [value]=\"query()\"\n [attr.placeholder]=\"text().searchLessons\"\n [attr.aria-label]=\"text().searchLessons\"\n (input)=\"onQueryInput($event)\"\n />\n </div>\n </div>\n }\n\n <div class=\"pecb-course-panel__scroll pecb-course-panel__list\">\n @if (isEmpty()) {\n <p class=\"pecb-course-panel__empty\">{{ noLessonsLabel() }}</p>\n } @else if (layout() === 'timeline') {\n <div class=\"pecb-course-panel__timeline\">\n @for (row of timelineRows(); track row.key) {\n @if (row.header; as header) {\n <div class=\"pecb-course-panel__timeline-head\">\n <span class=\"pecb-course-panel__timeline-label\">{{ sectionLabel(header.number) }}</span>\n <span class=\"pecb-course-panel__timeline-rule\" aria-hidden=\"true\"></span>\n <span class=\"pecb-course-panel__timeline-count\">{{ header.stats.done }}/{{ header.stats.total }}</span>\n </div>\n } @else {\n <ng-container *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: row.lesson }\"></ng-container>\n }\n }\n </div>\n } @else {\n @for (view of moduleViews(); track view.id) {\n @if (view.solo; as soloView) {\n <div class=\"pecb-course-panel__section pecb-course-panel__section--solo\">\n <ng-container\n *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: soloView, solo: true }\"\n ></ng-container>\n </div>\n } @else {\n <div\n class=\"pecb-course-panel__section\"\n [class.pecb-course-panel__section--open]=\"isModuleOpen(view.id)\"\n >\n <button\n type=\"button\"\n class=\"pecb-course-panel__section-head\"\n [class.pecb-course-panel__section-head--open]=\"isModuleOpen(view.id)\"\n [id]=\"uid + '-head-' + view.id\"\n [attr.aria-expanded]=\"isModuleOpen(view.id)\"\n [attr.aria-controls]=\"uid + '-region-' + view.id\"\n (click)=\"toggleModule(view)\"\n (keydown)=\"onModuleHeaderKeydown($event)\"\n >\n <span\n class=\"pecb-course-panel__section-badge\"\n [class.pecb-course-panel__section-badge--done]=\"view.allDone\"\n [class.pecb-course-panel__section-badge--active]=\"!view.allDone && (view.stats.playing || view.stats.started)\"\n aria-hidden=\"true\"\n >\n @if (view.allDone) {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n } @else if (view.stats.playing) {\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else {\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </span>\n\n <span class=\"pecb-course-panel__section-text\">\n <span class=\"pecb-course-panel__section-title\">{{ sectionLabel(view.number) }}</span>\n <span class=\"pecb-course-panel__section-meta\">\n <span>{{ view.statsLabel }}</span>\n @if (view.stats.durationSeconds > 0) {\n <span class=\"pecb-course-panel__section-duration\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <polyline points=\"12 7 12 12 15.5 14\" />\n </svg>\n {{ view.durationLabel }}\n </span>\n }\n </span>\n </span>\n\n <span class=\"pecb-course-panel__section-chevron\" aria-hidden=\"true\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"6 9 12 15 18 9\" />\n </svg>\n </span>\n </button>\n\n <div class=\"pecb-course-panel__section-wrap\">\n <div\n class=\"pecb-course-panel__section-clip\"\n [id]=\"uid + '-region-' + view.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"uid + '-head-' + view.id\"\n [attr.inert]=\"isModuleOpen(view.id) ? null : ''\"\n >\n <div class=\"pecb-course-panel__section-body\">\n @for (run of view.runs; track run.key) {\n @if (run.parts) {\n <p class=\"pecb-course-panel__run-title\">{{ run.base }}</p>\n <div class=\"pecb-course-panel__rail\">\n <span class=\"pecb-course-panel__rail-line\" aria-hidden=\"true\"></span>\n @for (lessonView of run.lessons; track lessonView.id) {\n <ng-container *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: lessonView }\"></ng-container>\n }\n </div>\n } @else {\n @for (lessonView of run.lessons; track lessonView.id) {\n <ng-container *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: lessonView }\"></ng-container>\n }\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n }\n }\n </div>\n </div>\n } @else {\n <!-- \u2500\u2500 Transcript tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"pecb-course-panel__body\"\n role=\"tabpanel\"\n [id]=\"uid + '-panel-transcript'\"\n [attr.aria-labelledby]=\"showTabs() ? uid + '-tab-transcript' : null\"\n >\n @if (showTranscriptSearch()) {\n <div class=\"pecb-course-panel__search\">\n <div class=\"pecb-course-panel__search-box\">\n <span class=\"pecb-course-panel__search-icon\" aria-hidden=\"true\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.2-3.2\" />\n </svg>\n </span>\n <input\n type=\"search\"\n class=\"pecb-course-panel__search-input\"\n [value]=\"transcriptQuery()\"\n [attr.placeholder]=\"text().searchTranscript\"\n [attr.aria-label]=\"text().searchTranscript\"\n (input)=\"onTranscriptQueryInput($event)\"\n />\n @if (transcriptQuery()) {\n <button\n type=\"button\"\n class=\"pecb-course-panel__search-clear\"\n [attr.aria-label]=\"text().clearSearch\"\n (click)=\"clearTranscriptQuery()\"\n >\u00D7</button>\n }\n </div>\n </div>\n }\n\n <div class=\"pecb-course-panel__scroll pecb-course-panel__transcript\" #transcriptScroll>\n @if (transcriptEmpty()) {\n <p class=\"pecb-course-panel__empty\">{{ noTranscriptLabel() }}</p>\n } @else {\n <div class=\"pecb-course-panel__cues\">\n @for (row of transcriptRows(); track row.key) {\n <button\n type=\"button\"\n class=\"pecb-course-panel__cue\"\n [class.pecb-course-panel__cue--current]=\"row.current\"\n [attr.data-current]=\"row.current\"\n [attr.aria-current]=\"row.current ? 'true' : null\"\n (click)=\"seekToCue(row.cue)\"\n >\n <span class=\"pecb-course-panel__cue-time\">{{ row.timeLabel }}</span>\n <span class=\"pecb-course-panel__cue-text\">{{ row.before }}@if (row.match) {<mark class=\"pecb-course-panel__mark\">{{ row.match }}</mark>}{{ row.after }}</span>\n </button>\n }\n </div>\n }\n </div>\n\n @if (showAutoscroll()) {\n <div class=\"pecb-course-panel__footer\">\n <label class=\"pecb-course-panel__autoscroll\">\n <input\n type=\"checkbox\"\n class=\"pecb-course-panel__autoscroll-input\"\n [checked]=\"autoscroll()\"\n (change)=\"onAutoscrollChange($event)\"\n />\n <span class=\"pecb-course-panel__autoscroll-box\" aria-hidden=\"true\">\n @if (autoscroll()) {\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n </span>\n <span class=\"pecb-course-panel__autoscroll-label\">{{ text().autoscroll }}</span>\n </label>\n </div>\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0;container-type:inline-size;container-name:pecb-course-panel}.pecb-course-panel{--_row-pad-y: 11px;--_row-pad-left: 14px;--_row-pad-right: 16px;--_row-gap: 13px;--_disc: 30px;--_section-pad-y: 15px;--_title-size: 13.5px;display:flex;flex-direction:column;height:100%;min-height:0;box-sizing:border-box;background:var(--pecb-course-panel-bg, #ffffff);font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);line-height:normal;color:#212427;overflow:hidden}.pecb-course-panel *,.pecb-course-panel *:before,.pecb-course-panel *:after{box-sizing:border-box}.pecb-course-panel--compact{--_row-pad-y: 8px;--_row-pad-left: 12px;--_row-pad-right: 14px;--_row-gap: 11px;--_disc: 26px;--_section-pad-y: 12px;--_title-size: 12.5px}.pecb-course-panel--accent-red{--pecb-course-panel-accent: #a11e29;--pecb-course-panel-accent-soft: rgba(161, 30, 41, .27)}.pecb-course-panel__tabs{display:flex;flex:0 0 auto;padding:0 20px;border-bottom:1px solid var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__tab{border:none;background:none;font:inherit;color:inherit;position:relative;flex:1;padding:16px 0 14px;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:14px;font-weight:500;color:#616161;cursor:pointer;transition:color .15s ease-in-out}.pecb-course-panel__tab:focus{outline:none}.pecb-course-panel__tab:hover{color:#212427}.pecb-course-panel__tab:focus-visible{outline:2px solid #a11e29;outline-offset:-3px;border-radius:6px}.pecb-course-panel__tab--active{font-weight:700;color:#a11e29}.pecb-course-panel__tab-indicator{position:absolute;left:0;right:0;bottom:-1px;height:2.5px;border-radius:3px;background:#a11e29}.pecb-course-panel__body{display:flex;flex:1 1 auto;flex-direction:column;min-height:0}.pecb-course-panel__scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#ececec transparent}.pecb-course-panel__scroll::-webkit-scrollbar{width:8px}.pecb-course-panel__scroll::-webkit-scrollbar-thumb{background:#ececec;border-radius:8px}.pecb-course-panel__scroll::-webkit-scrollbar-track{background:transparent}.pecb-course-panel__empty{margin:0;padding:40px;text-align:center;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;color:#919191}.pecb-course-panel__progress{display:flex;flex:0 0 auto;align-items:center;gap:16px;padding:16px 20px;border-bottom:1px solid var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__progress-ring{position:relative;flex:0 0 auto;width:56px;height:56px}.pecb-course-panel__progress-ring svg{display:block;transform:rotate(-90deg)}.pecb-course-panel__progress-track{stroke:#ececec}.pecb-course-panel__progress-value{stroke:var(--pecb-course-panel-accent, #212427);transition:stroke-dashoffset .5s ease}.pecb-course-panel__progress-percent{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:14px;font-weight:700;color:#212427}.pecb-course-panel__progress-text{display:flex;flex:1 1 auto;flex-direction:column;min-width:0}.pecb-course-panel__progress-title{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:14px;font-weight:700;color:#212427}.pecb-course-panel__progress-detail{margin-top:3px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:12px;color:#616161}.pecb-course-panel__certificate{border:none;background:none;font:inherit;color:inherit;display:inline-flex;flex:0 0 auto;align-items:center;gap:6px;padding:6px 12px;border-radius:9999px;background:#212427;color:#fff;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:12px;font-weight:600;cursor:pointer;transition:background .15s ease-in-out}.pecb-course-panel__certificate:focus{outline:none}.pecb-course-panel__certificate:hover{background:#313131}.pecb-course-panel__certificate:focus-visible{outline:2px solid #a11e29;outline-offset:2px}.pecb-course-panel__search{flex:0 0 auto;padding:12px 20px 10px}.pecb-course-panel__search-box{display:flex;align-items:center;gap:9px;min-height:40px;padding:9px 12px;border:1px solid #c2c2c2;border-radius:6px;background:#fff;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.pecb-course-panel__search-box:focus-within{border-color:#212427;box-shadow:0 0 0 3px #21242714}.pecb-course-panel__search-icon{display:flex;flex:0 0 auto;color:#919191}.pecb-course-panel__search-input{flex:1 1 auto;min-width:0;border:none;outline:none;background:transparent;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;color:#212427;appearance:none}.pecb-course-panel__search-input::placeholder{color:#919191}.pecb-course-panel__search-input::-webkit-search-cancel-button,.pecb-course-panel__search-input::-webkit-search-decoration{appearance:none;display:none}.pecb-course-panel__search-clear{padding:0;border:none;background:none;font:inherit;color:inherit;display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;color:#919191;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:17px;line-height:1;cursor:pointer}.pecb-course-panel__search-clear:focus{outline:none}.pecb-course-panel__search-clear:hover{background:#f4f4f4;color:#212427}.pecb-course-panel__search-clear:focus-visible{outline:2px solid #a11e29;outline-offset:1px}.pecb-course-panel__section{border-bottom:1px solid var(--pecb-course-panel-border, #ececec);background:transparent}.pecb-course-panel__section--open{background:#fff}.pecb-course-panel__section--solo{border-bottom:1px solid var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__section-head{padding:0;border:none;background:none;font:inherit;color:inherit;display:flex;align-items:center;gap:12px;width:100%;padding:var(--_section-pad-y) 16px;text-align:left;background:#f4f4f4;cursor:pointer;transition:background .15s ease-in-out}.pecb-course-panel__section-head:focus{outline:none}.pecb-course-panel__section-head:hover{background:#fff}.pecb-course-panel__section-head:focus-visible{outline:2px solid #a11e29;outline-offset:-2px}.pecb-course-panel__section-head--open{background:#ececec}.pecb-course-panel__section-head--open:hover{background:#f4f4f4}.pecb-course-panel__section-badge{display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:30px;height:30px;border-radius:8px;background:#c2c2c2;color:#fff;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;font-weight:700}.pecb-course-panel__section-badge--active{background:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__section-badge--done{background:#e6f4ea;color:green}.pecb-course-panel__section-text{display:flex;flex:1 1 auto;flex-direction:column;min-width:0}.pecb-course-panel__section-title{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13.5px;font-weight:600;line-height:1.3;color:#212427}.pecb-course-panel__section-meta{display:flex;align-items:center;gap:10px;margin-top:4px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11.5px;color:#919191}.pecb-course-panel__section-duration{display:inline-flex;align-items:center;gap:4px}.pecb-course-panel__section-chevron{display:flex;flex:0 0 auto;color:#919191;transform:rotate(-90deg);transition:transform .18s ease}.pecb-course-panel__section-head--open .pecb-course-panel__section-chevron{transform:rotate(0)}.pecb-course-panel__section-wrap{display:grid;grid-template-rows:0fr;transition:grid-template-rows .22s ease}.pecb-course-panel__section--open .pecb-course-panel__section-wrap{grid-template-rows:1fr}.pecb-course-panel__section-clip{overflow:hidden;min-height:0}.pecb-course-panel__section-body{display:flex;flex-direction:column;gap:2px;padding:6px 12px 10px var(--_row-pad-left)}.pecb-course-panel--compact .pecb-course-panel__section-body{padding:4px 10px 8px var(--_row-pad-left)}.pecb-course-panel__run-title{margin:0;padding:9px 8px 5px 12px;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;font-weight:600;line-height:1.35;color:#212427;text-wrap:pretty}.pecb-course-panel__rail{position:relative}.pecb-course-panel__rail-line{position:absolute;left:calc(var(--_row-pad-left) + var(--_disc) / 2 - 1px);top:calc(var(--_row-pad-y) + var(--_disc) / 2);bottom:calc(var(--_row-pad-y) + var(--_disc) / 2);z-index:1;width:2px;border-radius:2px;background:#ececec}.pecb-course-panel__row{padding:0;border:none;background:none;font:inherit;color:inherit;position:relative;display:flex;align-items:center;gap:var(--_row-gap);width:100%;padding:var(--_row-pad-y) var(--_row-pad-right) var(--_row-pad-y) var(--_row-pad-left);border-radius:8px;text-align:left;background:transparent;cursor:pointer;transition:background .14s ease,box-shadow .14s ease}.pecb-course-panel__row:focus{outline:none}.pecb-course-panel__row:hover:not(:disabled):not(.pecb-course-panel__row--active){background:#fafafa}.pecb-course-panel__row:focus-visible{outline:2px solid #a11e29;outline-offset:-2px}.pecb-course-panel__row:disabled{cursor:not-allowed}.pecb-course-panel__row--locked{opacity:.72}.pecb-course-panel__row--active{background:#fff;box-shadow:inset 0 0 0 1px var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__row--active.pecb-course-panel__row--completed{background:#e6f4ea}.pecb-course-panel__row--active.pecb-course-panel__row--started{background:#fff4e5}.pecb-course-panel__row--active.pecb-course-panel__row--playing{background:#fbeaec;box-shadow:inset 0 0 0 1px var(--pecb-course-panel-accent-soft, rgba(33, 36, 39, .27))}.pecb-course-panel__row--active.pecb-course-panel__row--quiz{background:#eee8ff}.pecb-course-panel__row--solo{gap:12px;padding:var(--_section-pad-y) 16px;border-radius:0}.pecb-course-panel__row-marker{position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;background:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__row--solo .pecb-course-panel__row-marker{top:10px;bottom:10px}.pecb-course-panel__row--solo .pecb-course-panel__row-title{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13.5px;font-weight:600;line-height:1.3}.pecb-course-panel__solo-play{display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#f4f4f4;color:#616161}.pecb-course-panel__disc-col{position:relative;display:flex;flex:0 0 auto;flex-direction:column;align-items:center;align-self:stretch}.pecb-course-panel__disc{position:relative;z-index:2;display:flex;flex:0 0 auto;align-items:center;justify-content:center;border-radius:50%;background:#fff;border:none;color:#919191}.pecb-course-panel__disc svg{display:block}.pecb-course-panel__disc--plain{border:1.5px solid #ececec}.pecb-course-panel__disc--completed{background:#57bc57;color:#fff}.pecb-course-panel__disc--playing{background:var(--pecb-course-panel-accent, #212427);color:#fff;box-shadow:0 0 0 4px var(--pecb-course-panel-accent-soft, rgba(33, 36, 39, .27))}.pecb-course-panel__disc--started{background:#fff;color:#ff9c4a}.pecb-course-panel__disc--locked{color:#c2c2c2}.pecb-course-panel__ring{position:absolute;inset:0;transform:rotate(-90deg)}.pecb-course-panel__ring-track{stroke:#ececec}.pecb-course-panel__ring-value{stroke:#ff9c4a;transition:stroke-dashoffset .3s ease}.pecb-course-panel__disc-glyph{position:relative;display:flex;color:#ff9c4a}.pecb-course-panel__connector{flex:1 1 auto;width:2px;margin-top:2px;border-radius:2px;background:#ececec}.pecb-course-panel__connector--done{background:#57bc57}.pecb-course-panel__row-body{display:block;flex:1 1 auto;min-width:0}.pecb-course-panel__quiz-badge-line{display:flex;align-items:center;min-height:18px;margin-bottom:5px}.pecb-course-panel__quiz-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:9999px;background:#8e6df0;color:#fff;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:9.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase}.pecb-course-panel__row-title{display:block;margin-bottom:4px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:var(--_title-size);font-weight:500;line-height:1.35;color:#212427;text-wrap:pretty;overflow-wrap:anywhere}.pecb-course-panel--compact .pecb-course-panel__row-title{margin-bottom:2px}.pecb-course-panel__row--active .pecb-course-panel__row-title{font-weight:600}.pecb-course-panel__row-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.pecb-course-panel__meta-primary{display:inline-flex;align-items:center;gap:5px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11.5px;font-weight:500;color:#919191}.pecb-course-panel__row--quiz .pecb-course-panel__meta-primary{color:#8e6df0}.pecb-course-panel__meta-result{display:inline-flex;align-items:center;gap:4px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11px;font-weight:600;color:#ff9c4a}.pecb-course-panel__meta-result--passed{color:#57bc57}.pecb-course-panel__meta-time{display:inline-flex;align-items:center;gap:5px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11px;font-weight:600;color:#ff9c4a}.pecb-course-panel__row--playing .pecb-course-panel__meta-time{color:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__bar{display:block;margin-top:7px;height:4px;border-radius:4px;background:#ececec;overflow:hidden}.pecb-course-panel__bar-fill{display:block;height:100%;border-radius:4px;background:#ff9c4a;transition:width .2s linear}.pecb-course-panel__row--playing .pecb-course-panel__bar-fill{background:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__timeline{padding:6px 14px 16px}.pecb-course-panel--compact .pecb-course-panel__timeline{padding:4px 12px 12px}.pecb-course-panel__timeline-head{display:flex;align-items:center;gap:10px;padding:18px 2px 8px}.pecb-course-panel--compact .pecb-course-panel__timeline-head{padding:14px 2px 6px}.pecb-course-panel__timeline-label{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:#919191}.pecb-course-panel__timeline-rule{flex:1 1 auto;height:1px;background:var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__timeline-count{font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11px;color:#919191}.pecb-course-panel__transcript{padding:0 14px}.pecb-course-panel__cues{padding:8px 6px 20px}.pecb-course-panel__cue{border:none;background:none;font:inherit;color:inherit;display:flex;gap:14px;width:100%;padding:10px 12px;border-radius:8px;text-align:left;background:transparent;cursor:pointer;transition:background .14s ease}.pecb-course-panel__cue:focus{outline:none}.pecb-course-panel__cue:hover:not(.pecb-course-panel__cue--current){background:#fafafa}.pecb-course-panel__cue:focus-visible{outline:2px solid #a11e29;outline-offset:-2px}.pecb-course-panel__cue--current{background:#fbeaec}.pecb-course-panel__cue-time{flex:0 0 auto;width:38px;padding-top:1px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;color:#919191}.pecb-course-panel__cue--current .pecb-course-panel__cue-time{color:#a11e29}.pecb-course-panel__cue-text{font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13.5px;line-height:1.55;color:#313131;text-wrap:pretty;overflow-wrap:anywhere}.pecb-course-panel__cue--current .pecb-course-panel__cue-text{color:#212427}.pecb-course-panel__mark{padding:0 1px;border-radius:3px;background:#fce3b4;color:inherit}.pecb-course-panel__footer{flex:0 0 auto;padding:12px 20px;border-top:1px solid var(--pecb-course-panel-border, #ececec);background:var(--pecb-course-panel-bg, #ffffff)}.pecb-course-panel__autoscroll{display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.pecb-course-panel__autoscroll-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.pecb-course-panel__autoscroll-box{display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:18px;height:18px;border:1.5px solid #c2c2c2;border-radius:5px;background:#fff;color:#fff;transition:all .14s ease}.pecb-course-panel__autoscroll-input:checked+.pecb-course-panel__autoscroll-box{border:1px solid #a11e29;background:#a11e29}.pecb-course-panel__autoscroll-input:focus-visible+.pecb-course-panel__autoscroll-box{outline:2px solid #a11e29;outline-offset:2px}.pecb-course-panel__autoscroll-label{font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;font-weight:500;color:#616161}@container pecb-course-panel (max-width: 380px){.pecb-course-panel{--_row-pad-right: 12px}.pecb-course-panel__tabs{padding:0 12px}.pecb-course-panel__tab{padding:12px 0 10px;font-size:13px}.pecb-course-panel__search{padding:10px 12px 8px}.pecb-course-panel__progress{flex-wrap:wrap;gap:12px;padding:12px 16px}.pecb-course-panel__section-head{gap:10px;padding:12px}.pecb-course-panel__section-body{padding-right:8px}.pecb-course-panel__transcript{padding:0 8px}.pecb-course-panel__cue{gap:10px;padding:8px 10px}.pecb-course-panel__cue-text{font-size:13px}.pecb-course-panel__footer{padding:10px 16px}.pecb-course-panel__timeline{padding:4px 10px 12px}}@media (hover: none) and (pointer: coarse){.pecb-course-panel__row,.pecb-course-panel__cue{min-height:44px}}@media (prefers-reduced-motion: reduce){.pecb-course-panel *,.pecb-course-panel *:before,.pecb-course-panel *:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12269
+ }
12270
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CourseContentPanelComponent, decorators: [{
12271
+ type: Component,
12272
+ args: [{ selector: 'pecb-course-content-panel', standalone: true, imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'pecb-course-panel-host' }, template: "<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Lesson row \u2014 one <button> per lesson, shared by the grouped, parts-rail,\n timeline and solo renderings.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<ng-template #lessonRowTpl let-vm let-solo=\"solo\">\n <button\n type=\"button\"\n class=\"pecb-course-panel__row\"\n [class.pecb-course-panel__row--active]=\"vm.active\"\n [class.pecb-course-panel__row--quiz]=\"vm.isQuiz\"\n [class.pecb-course-panel__row--playing]=\"vm.isPlaying\"\n [class.pecb-course-panel__row--started]=\"vm.isStarted\"\n [class.pecb-course-panel__row--completed]=\"vm.isCompleted\"\n [class.pecb-course-panel__row--locked]=\"vm.isLocked\"\n [class.pecb-course-panel__row--solo]=\"!!solo\"\n [disabled]=\"vm.isLocked\"\n [attr.aria-current]=\"vm.active ? 'true' : null\"\n [attr.aria-label]=\"vm.ariaLabel\"\n (click)=\"selectLesson(vm.lesson)\"\n >\n @if (vm.active && vm.isPlaying) {\n <span class=\"pecb-course-panel__row-marker\" aria-hidden=\"true\"></span>\n }\n\n <!-- Status disc (+ optional timeline connector) -->\n <span class=\"pecb-course-panel__disc-col\">\n <span\n class=\"pecb-course-panel__disc\"\n [class.pecb-course-panel__disc--completed]=\"vm.isCompleted\"\n [class.pecb-course-panel__disc--playing]=\"vm.isPlaying\"\n [class.pecb-course-panel__disc--started]=\"vm.isStarted\"\n [class.pecb-course-panel__disc--locked]=\"vm.isLocked\"\n [class.pecb-course-panel__disc--plain]=\"!vm.isCompleted && !vm.isPlaying && !vm.isStarted\"\n [style.width.px]=\"vm.discSize\"\n [style.height.px]=\"vm.discSize\"\n aria-hidden=\"true\"\n >\n @if (vm.isCompleted) {\n <svg [attr.width]=\"vm.checkSize\" [attr.height]=\"vm.checkSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n } @else if (vm.isPlaying) {\n <svg [attr.width]=\"vm.pauseSize\" [attr.height]=\"vm.pauseSize\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else if (vm.isStarted) {\n <svg class=\"pecb-course-panel__ring\" [attr.width]=\"vm.discSize\" [attr.height]=\"vm.discSize\" [attr.viewBox]=\"vm.ringViewBox\">\n <circle\n class=\"pecb-course-panel__ring-track\"\n [attr.cx]=\"vm.discCenter\" [attr.cy]=\"vm.discCenter\" [attr.r]=\"vm.ringRadius\"\n fill=\"none\" stroke-width=\"3\" />\n <circle\n class=\"pecb-course-panel__ring-value\"\n [attr.cx]=\"vm.discCenter\" [attr.cy]=\"vm.discCenter\" [attr.r]=\"vm.ringRadius\"\n fill=\"none\" stroke-width=\"3\" stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"vm.ringCircumference\"\n [attr.stroke-dashoffset]=\"vm.ringOffset\" />\n </svg>\n <span class=\"pecb-course-panel__disc-glyph\">\n @if (vm.isQuiz) {\n <svg [attr.width]=\"vm.quizIconSize\" [attr.height]=\"vm.quizIconSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M9.5 9.2a2.5 2.5 0 1 1 3.4 2.3c-.6.3-.9.7-.9 1.4v.4\" />\n <circle cx=\"12\" cy=\"16.3\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n } @else {\n <svg [attr.width]=\"vm.playIconSize\" [attr.height]=\"vm.playIconSize\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </span>\n } @else if (vm.isLocked) {\n <svg [attr.width]=\"vm.checkSize\" [attr.height]=\"vm.checkSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"5\" y=\"11\" width=\"14\" height=\"9\" rx=\"2\" />\n <path d=\"M8 11V8a4 4 0 0 1 8 0v3\" />\n </svg>\n } @else if (vm.isQuiz) {\n <svg [attr.width]=\"vm.quizIconSize\" [attr.height]=\"vm.quizIconSize\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M9.5 9.2a2.5 2.5 0 1 1 3.4 2.3c-.6.3-.9.7-.9 1.4v.4\" />\n <circle cx=\"12\" cy=\"16.3\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n } @else {\n <svg [attr.width]=\"vm.playIconSize\" [attr.height]=\"vm.playIconSize\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </span>\n\n @if (vm.connector) {\n <span\n class=\"pecb-course-panel__connector\"\n [class.pecb-course-panel__connector--done]=\"vm.connectorDone\"\n aria-hidden=\"true\"\n ></span>\n }\n </span>\n\n <!-- Body -->\n <span class=\"pecb-course-panel__row-body\">\n @if (vm.isQuiz) {\n <span class=\"pecb-course-panel__quiz-badge-line\">\n <span class=\"pecb-course-panel__quiz-badge\">{{ text().quiz }}</span>\n </span>\n }\n\n @if (vm.displayTitle) {\n <span class=\"pecb-course-panel__row-title\">{{ vm.displayTitle }}</span>\n }\n\n <span class=\"pecb-course-panel__row-meta\">\n <span class=\"pecb-course-panel__meta-primary\">\n @if (vm.isQuiz) {\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M9.5 9.2a2.5 2.5 0 1 1 3.4 2.3c-.6.3-.9.7-.9 1.4v.4\" />\n <circle cx=\"12\" cy=\"16.3\" r=\".6\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n {{ vm.questionsLabel }}\n } @else {\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <polyline points=\"12 7 12 12 15.5 14\" />\n </svg>\n {{ vm.durationLabel }}\n }\n </span>\n\n @if (vm.quizResultLabel) {\n <span\n class=\"pecb-course-panel__meta-result\"\n [class.pecb-course-panel__meta-result--passed]=\"vm.isCompleted\"\n >\n @if (vm.isCompleted) {\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n {{ vm.quizResultLabel }}\n </span>\n }\n\n @if (vm.timeLabel) {\n <span class=\"pecb-course-panel__meta-time\">\n @if (vm.isStarted) {\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n {{ vm.timeLabel }}\n </span>\n }\n </span>\n\n @if (vm.showBar) {\n <span class=\"pecb-course-panel__bar\" aria-hidden=\"true\">\n <span class=\"pecb-course-panel__bar-fill\" [style.width.%]=\"vm.barPercent\"></span>\n </span>\n }\n </span>\n\n @if (solo) {\n <span class=\"pecb-course-panel__solo-play\" aria-hidden=\"true\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n </span>\n }\n </button>\n</ng-template>\n\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Panel\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<div [class]=\"hostClasses()\">\n @if (showTabs()) {\n <div class=\"pecb-course-panel__tabs\" role=\"tablist\" [attr.aria-label]=\"text().lessonList\">\n <button\n type=\"button\"\n role=\"tab\"\n class=\"pecb-course-panel__tab\"\n [class.pecb-course-panel__tab--active]=\"tab() === 'content'\"\n [id]=\"uid + '-tab-content'\"\n [attr.aria-selected]=\"tab() === 'content'\"\n [attr.aria-controls]=\"uid + '-panel-content'\"\n [attr.tabindex]=\"tab() === 'content' ? 0 : -1\"\n (click)=\"setTab('content')\"\n (keydown)=\"onTabKeydown($event)\"\n >\n {{ text().contentTab }}\n @if (tab() === 'content') {\n <span class=\"pecb-course-panel__tab-indicator\" aria-hidden=\"true\"></span>\n }\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n class=\"pecb-course-panel__tab\"\n [class.pecb-course-panel__tab--active]=\"tab() === 'transcript'\"\n [id]=\"uid + '-tab-transcript'\"\n [attr.aria-selected]=\"tab() === 'transcript'\"\n [attr.aria-controls]=\"uid + '-panel-transcript'\"\n [attr.tabindex]=\"tab() === 'transcript' ? 0 : -1\"\n (click)=\"setTab('transcript')\"\n (keydown)=\"onTabKeydown($event)\"\n >\n {{ text().transcriptTab }}\n @if (tab() === 'transcript') {\n <span class=\"pecb-course-panel__tab-indicator\" aria-hidden=\"true\"></span>\n }\n </button>\n </div>\n }\n\n <!-- \u2500\u2500 Content tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (tab() === 'content') {\n <div\n class=\"pecb-course-panel__body\"\n role=\"tabpanel\"\n [id]=\"uid + '-panel-content'\"\n [attr.aria-labelledby]=\"showTabs() ? uid + '-tab-content' : null\"\n >\n @if (showProgress()) {\n <div class=\"pecb-course-panel__progress\">\n <span class=\"pecb-course-panel__progress-ring\">\n <svg width=\"56\" height=\"56\" viewBox=\"0 0 56 56\" aria-hidden=\"true\">\n <circle class=\"pecb-course-panel__progress-track\" cx=\"28\" cy=\"28\" [attr.r]=\"progressRing().radius\" fill=\"none\" stroke-width=\"5\" />\n <circle\n class=\"pecb-course-panel__progress-value\"\n cx=\"28\" cy=\"28\" [attr.r]=\"progressRing().radius\" fill=\"none\" stroke-width=\"5\" stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"progressRing().circumference\"\n [attr.stroke-dashoffset]=\"progressRing().offset\" />\n </svg>\n <span class=\"pecb-course-panel__progress-percent\">{{ progress().percent }}%</span>\n </span>\n <span class=\"pecb-course-panel__progress-text\">\n <span class=\"pecb-course-panel__progress-title\">{{ text().courseProgress }}</span>\n <span class=\"pecb-course-panel__progress-detail\">{{ progressDetail() }}</span>\n </span>\n @if (showCertificate()) {\n <button type=\"button\" class=\"pecb-course-panel__certificate\" (click)=\"certificateClick.emit()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"8\" r=\"5\" />\n <path d=\"M8.5 12.5 7 21l5-2.5L17 21l-1.5-8.5\" />\n </svg>\n {{ text().certificate }}\n </button>\n }\n </div>\n }\n\n @if (showSearch()) {\n <div class=\"pecb-course-panel__search\">\n <div class=\"pecb-course-panel__search-box\">\n <span class=\"pecb-course-panel__search-icon\" aria-hidden=\"true\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.2-3.2\" />\n </svg>\n </span>\n <input\n type=\"search\"\n class=\"pecb-course-panel__search-input\"\n [value]=\"query()\"\n [attr.placeholder]=\"text().searchLessons\"\n [attr.aria-label]=\"text().searchLessons\"\n (input)=\"onQueryInput($event)\"\n />\n </div>\n </div>\n }\n\n <div class=\"pecb-course-panel__scroll pecb-course-panel__list\">\n @if (isEmpty()) {\n <p class=\"pecb-course-panel__empty\">{{ noLessonsLabel() }}</p>\n } @else if (layout() === 'timeline') {\n <div class=\"pecb-course-panel__timeline\">\n @for (row of timelineRows(); track row.key) {\n @if (row.header; as header) {\n <div class=\"pecb-course-panel__timeline-head\">\n <span class=\"pecb-course-panel__timeline-label\">{{ sectionLabel(header.number) }}</span>\n <span class=\"pecb-course-panel__timeline-rule\" aria-hidden=\"true\"></span>\n <span class=\"pecb-course-panel__timeline-count\">{{ header.stats.done }}/{{ header.stats.total }}</span>\n </div>\n } @else {\n <ng-container *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: row.lesson }\"></ng-container>\n }\n }\n </div>\n } @else {\n @for (view of moduleViews(); track view.id) {\n @if (view.solo; as soloView) {\n <div class=\"pecb-course-panel__section pecb-course-panel__section--solo\">\n <ng-container\n *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: soloView, solo: true }\"\n ></ng-container>\n </div>\n } @else {\n <div\n class=\"pecb-course-panel__section\"\n [class.pecb-course-panel__section--open]=\"isModuleOpen(view.id)\"\n >\n <button\n type=\"button\"\n class=\"pecb-course-panel__section-head\"\n [class.pecb-course-panel__section-head--open]=\"isModuleOpen(view.id)\"\n [id]=\"uid + '-head-' + view.id\"\n [attr.aria-expanded]=\"isModuleOpen(view.id)\"\n [attr.aria-controls]=\"uid + '-region-' + view.id\"\n (click)=\"toggleModule(view)\"\n (keydown)=\"onModuleHeaderKeydown($event)\"\n >\n <span\n class=\"pecb-course-panel__section-badge\"\n [class.pecb-course-panel__section-badge--done]=\"view.allDone\"\n [class.pecb-course-panel__section-badge--active]=\"!view.allDone && (view.stats.playing || view.stats.started)\"\n aria-hidden=\"true\"\n >\n @if (view.allDone) {\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n } @else if (view.stats.playing) {\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </svg>\n } @else {\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <polygon points=\"6 4 20 12 6 20 6 4\" />\n </svg>\n }\n </span>\n\n <span class=\"pecb-course-panel__section-text\">\n <span class=\"pecb-course-panel__section-title\">{{ sectionLabel(view.number) }}</span>\n <span class=\"pecb-course-panel__section-meta\">\n <span>{{ view.statsLabel }}</span>\n @if (view.stats.durationSeconds > 0) {\n <span class=\"pecb-course-panel__section-duration\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <polyline points=\"12 7 12 12 15.5 14\" />\n </svg>\n {{ view.durationLabel }}\n </span>\n }\n </span>\n </span>\n\n <span class=\"pecb-course-panel__section-chevron\" aria-hidden=\"true\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"6 9 12 15 18 9\" />\n </svg>\n </span>\n </button>\n\n <div class=\"pecb-course-panel__section-wrap\">\n <div\n class=\"pecb-course-panel__section-clip\"\n [id]=\"uid + '-region-' + view.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"uid + '-head-' + view.id\"\n [attr.inert]=\"isModuleOpen(view.id) ? null : ''\"\n >\n <div class=\"pecb-course-panel__section-body\">\n @for (run of view.runs; track run.key) {\n @if (run.parts) {\n <p class=\"pecb-course-panel__run-title\">{{ run.base }}</p>\n <div class=\"pecb-course-panel__rail\">\n <span class=\"pecb-course-panel__rail-line\" aria-hidden=\"true\"></span>\n @for (lessonView of run.lessons; track lessonView.id) {\n <ng-container *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: lessonView }\"></ng-container>\n }\n </div>\n } @else {\n @for (lessonView of run.lessons; track lessonView.id) {\n <ng-container *ngTemplateOutlet=\"lessonRowTpl; context: { $implicit: lessonView }\"></ng-container>\n }\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n }\n }\n </div>\n </div>\n } @else {\n <!-- \u2500\u2500 Transcript tab \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"pecb-course-panel__body\"\n role=\"tabpanel\"\n [id]=\"uid + '-panel-transcript'\"\n [attr.aria-labelledby]=\"showTabs() ? uid + '-tab-transcript' : null\"\n >\n @if (showTranscriptSearch()) {\n <div class=\"pecb-course-panel__search\">\n <div class=\"pecb-course-panel__search-box\">\n <span class=\"pecb-course-panel__search-icon\" aria-hidden=\"true\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.2-3.2\" />\n </svg>\n </span>\n <input\n type=\"search\"\n class=\"pecb-course-panel__search-input\"\n [value]=\"transcriptQuery()\"\n [attr.placeholder]=\"text().searchTranscript\"\n [attr.aria-label]=\"text().searchTranscript\"\n (input)=\"onTranscriptQueryInput($event)\"\n />\n @if (transcriptQuery()) {\n <button\n type=\"button\"\n class=\"pecb-course-panel__search-clear\"\n [attr.aria-label]=\"text().clearSearch\"\n (click)=\"clearTranscriptQuery()\"\n >\u00D7</button>\n }\n </div>\n </div>\n }\n\n <div class=\"pecb-course-panel__scroll pecb-course-panel__transcript\" #transcriptScroll>\n @if (transcriptEmpty()) {\n <p class=\"pecb-course-panel__empty\">{{ noTranscriptLabel() }}</p>\n } @else {\n <div class=\"pecb-course-panel__cues\">\n @for (row of transcriptRows(); track row.key) {\n <button\n type=\"button\"\n class=\"pecb-course-panel__cue\"\n [class.pecb-course-panel__cue--current]=\"row.current\"\n [attr.data-current]=\"row.current\"\n [attr.aria-current]=\"row.current ? 'true' : null\"\n (click)=\"seekToCue(row.cue)\"\n >\n <span class=\"pecb-course-panel__cue-time\">{{ row.timeLabel }}</span>\n <span class=\"pecb-course-panel__cue-text\">{{ row.before }}@if (row.match) {<mark class=\"pecb-course-panel__mark\">{{ row.match }}</mark>}{{ row.after }}</span>\n </button>\n }\n </div>\n }\n </div>\n\n @if (showAutoscroll()) {\n <div class=\"pecb-course-panel__footer\">\n <label class=\"pecb-course-panel__autoscroll\">\n <input\n type=\"checkbox\"\n class=\"pecb-course-panel__autoscroll-input\"\n [checked]=\"autoscroll()\"\n (change)=\"onAutoscrollChange($event)\"\n />\n <span class=\"pecb-course-panel__autoscroll-box\" aria-hidden=\"true\">\n @if (autoscroll()) {\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n }\n </span>\n <span class=\"pecb-course-panel__autoscroll-label\">{{ text().autoscroll }}</span>\n </label>\n </div>\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0;container-type:inline-size;container-name:pecb-course-panel}.pecb-course-panel{--_row-pad-y: 11px;--_row-pad-left: 14px;--_row-pad-right: 16px;--_row-gap: 13px;--_disc: 30px;--_section-pad-y: 15px;--_title-size: 13.5px;display:flex;flex-direction:column;height:100%;min-height:0;box-sizing:border-box;background:var(--pecb-course-panel-bg, #ffffff);font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);line-height:normal;color:#212427;overflow:hidden}.pecb-course-panel *,.pecb-course-panel *:before,.pecb-course-panel *:after{box-sizing:border-box}.pecb-course-panel--compact{--_row-pad-y: 8px;--_row-pad-left: 12px;--_row-pad-right: 14px;--_row-gap: 11px;--_disc: 26px;--_section-pad-y: 12px;--_title-size: 12.5px}.pecb-course-panel--accent-red{--pecb-course-panel-accent: #a11e29;--pecb-course-panel-accent-soft: rgba(161, 30, 41, .27)}.pecb-course-panel__tabs{display:flex;flex:0 0 auto;padding:0 20px;border-bottom:1px solid var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__tab{border:none;background:none;font:inherit;color:inherit;position:relative;flex:1;padding:16px 0 14px;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:14px;font-weight:500;color:#616161;cursor:pointer;transition:color .15s ease-in-out}.pecb-course-panel__tab:focus{outline:none}.pecb-course-panel__tab:hover{color:#212427}.pecb-course-panel__tab:focus-visible{outline:2px solid #a11e29;outline-offset:-3px;border-radius:6px}.pecb-course-panel__tab--active{font-weight:700;color:#a11e29}.pecb-course-panel__tab-indicator{position:absolute;left:0;right:0;bottom:-1px;height:2.5px;border-radius:3px;background:#a11e29}.pecb-course-panel__body{display:flex;flex:1 1 auto;flex-direction:column;min-height:0}.pecb-course-panel__scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#ececec transparent}.pecb-course-panel__scroll::-webkit-scrollbar{width:8px}.pecb-course-panel__scroll::-webkit-scrollbar-thumb{background:#ececec;border-radius:8px}.pecb-course-panel__scroll::-webkit-scrollbar-track{background:transparent}.pecb-course-panel__empty{margin:0;padding:40px;text-align:center;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;color:#919191}.pecb-course-panel__progress{display:flex;flex:0 0 auto;align-items:center;gap:16px;padding:16px 20px;border-bottom:1px solid var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__progress-ring{position:relative;flex:0 0 auto;width:56px;height:56px}.pecb-course-panel__progress-ring svg{display:block;transform:rotate(-90deg)}.pecb-course-panel__progress-track{stroke:#ececec}.pecb-course-panel__progress-value{stroke:var(--pecb-course-panel-accent, #212427);transition:stroke-dashoffset .5s ease}.pecb-course-panel__progress-percent{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:14px;font-weight:700;color:#212427}.pecb-course-panel__progress-text{display:flex;flex:1 1 auto;flex-direction:column;min-width:0}.pecb-course-panel__progress-title{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:14px;font-weight:700;color:#212427}.pecb-course-panel__progress-detail{margin-top:3px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:12px;color:#616161}.pecb-course-panel__certificate{border:none;background:none;font:inherit;color:inherit;display:inline-flex;flex:0 0 auto;align-items:center;gap:6px;padding:6px 12px;border-radius:9999px;background:#212427;color:#fff;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:12px;font-weight:600;cursor:pointer;transition:background .15s ease-in-out}.pecb-course-panel__certificate:focus{outline:none}.pecb-course-panel__certificate:hover{background:#313131}.pecb-course-panel__certificate:focus-visible{outline:2px solid #a11e29;outline-offset:2px}.pecb-course-panel__search{flex:0 0 auto;padding:12px 20px 10px}.pecb-course-panel__search-box{display:flex;align-items:center;gap:9px;min-height:40px;padding:9px 12px;border:1px solid #c2c2c2;border-radius:6px;background:#fff;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.pecb-course-panel__search-box:focus-within{border-color:#212427;box-shadow:0 0 0 3px #21242714}.pecb-course-panel__search-icon{display:flex;flex:0 0 auto;color:#919191}.pecb-course-panel__search-input{flex:1 1 auto;min-width:0;border:none;outline:none;background:transparent;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;color:#212427;appearance:none}.pecb-course-panel__search-input::placeholder{color:#919191}.pecb-course-panel__search-input::-webkit-search-cancel-button,.pecb-course-panel__search-input::-webkit-search-decoration{appearance:none;display:none}.pecb-course-panel__search-clear{padding:0;border:none;background:none;font:inherit;color:inherit;display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;color:#919191;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:17px;line-height:1;cursor:pointer}.pecb-course-panel__search-clear:focus{outline:none}.pecb-course-panel__search-clear:hover{background:#f4f4f4;color:#212427}.pecb-course-panel__search-clear:focus-visible{outline:2px solid #a11e29;outline-offset:1px}.pecb-course-panel__section{border-bottom:1px solid var(--pecb-course-panel-border, #ececec);background:transparent}.pecb-course-panel__section--open{background:#fff}.pecb-course-panel__section--solo{border-bottom:1px solid var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__section-head{padding:0;border:none;background:none;font:inherit;color:inherit;display:flex;align-items:center;gap:12px;width:100%;padding:var(--_section-pad-y) 16px;text-align:left;background:#f4f4f4;cursor:pointer;transition:background .15s ease-in-out}.pecb-course-panel__section-head:focus{outline:none}.pecb-course-panel__section-head:hover{background:#fff}.pecb-course-panel__section-head:focus-visible{outline:2px solid #a11e29;outline-offset:-2px}.pecb-course-panel__section-head--open{background:#ececec}.pecb-course-panel__section-head--open:hover{background:#f4f4f4}.pecb-course-panel__section-badge{display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:30px;height:30px;border-radius:8px;background:#c2c2c2;color:#fff;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;font-weight:700}.pecb-course-panel__section-badge--active{background:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__section-badge--done{background:#e6f4ea;color:green}.pecb-course-panel__section-text{display:flex;flex:1 1 auto;flex-direction:column;min-width:0}.pecb-course-panel__section-title{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13.5px;font-weight:600;line-height:1.3;color:#212427}.pecb-course-panel__section-meta{display:flex;align-items:center;gap:10px;margin-top:4px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11.5px;color:#919191}.pecb-course-panel__section-duration{display:inline-flex;align-items:center;gap:4px}.pecb-course-panel__section-chevron{display:flex;flex:0 0 auto;color:#919191;transform:rotate(-90deg);transition:transform .18s ease}.pecb-course-panel__section-head--open .pecb-course-panel__section-chevron{transform:rotate(0)}.pecb-course-panel__section-wrap{display:grid;grid-template-rows:0fr;transition:grid-template-rows .22s ease}.pecb-course-panel__section--open .pecb-course-panel__section-wrap{grid-template-rows:1fr}.pecb-course-panel__section-clip{overflow:hidden;min-height:0}.pecb-course-panel__section-body{display:flex;flex-direction:column;gap:2px;padding:6px 12px 10px var(--_row-pad-left)}.pecb-course-panel--compact .pecb-course-panel__section-body{padding:4px 10px 8px var(--_row-pad-left)}.pecb-course-panel__run-title{margin:0;padding:9px 8px 5px 12px;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;font-weight:600;line-height:1.35;color:#212427;text-wrap:pretty}.pecb-course-panel__rail{position:relative}.pecb-course-panel__rail-line{position:absolute;left:calc(var(--_row-pad-left) + var(--_disc) / 2 - 1px);top:calc(var(--_row-pad-y) + var(--_disc) / 2);bottom:calc(var(--_row-pad-y) + var(--_disc) / 2);z-index:1;width:2px;border-radius:2px;background:#ececec}.pecb-course-panel__row{padding:0;border:none;background:none;font:inherit;color:inherit;position:relative;display:flex;align-items:center;gap:var(--_row-gap);width:100%;padding:var(--_row-pad-y) var(--_row-pad-right) var(--_row-pad-y) var(--_row-pad-left);border-radius:8px;text-align:left;background:transparent;cursor:pointer;transition:background .14s ease,box-shadow .14s ease}.pecb-course-panel__row:focus{outline:none}.pecb-course-panel__row:hover:not(:disabled):not(.pecb-course-panel__row--active){background:#fafafa}.pecb-course-panel__row:focus-visible{outline:2px solid #a11e29;outline-offset:-2px}.pecb-course-panel__row:disabled{cursor:not-allowed}.pecb-course-panel__row--locked{opacity:.72}.pecb-course-panel__row--active{background:#fff;box-shadow:inset 0 0 0 1px var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__row--active.pecb-course-panel__row--completed{background:#e6f4ea}.pecb-course-panel__row--active.pecb-course-panel__row--started{background:#fff4e5}.pecb-course-panel__row--active.pecb-course-panel__row--playing{background:#fbeaec;box-shadow:inset 0 0 0 1px var(--pecb-course-panel-accent-soft, rgba(33, 36, 39, .27))}.pecb-course-panel__row--active.pecb-course-panel__row--quiz{background:#eee8ff}.pecb-course-panel__row--solo{gap:12px;padding:var(--_section-pad-y) 16px;border-radius:0}.pecb-course-panel__row-marker{position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;background:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__row--solo .pecb-course-panel__row-marker{top:10px;bottom:10px}.pecb-course-panel__row--solo .pecb-course-panel__row-title{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13.5px;font-weight:600;line-height:1.3}.pecb-course-panel__solo-play{display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:#f4f4f4;color:#616161}.pecb-course-panel__disc-col{position:relative;display:flex;flex:0 0 auto;flex-direction:column;align-items:center;align-self:stretch}.pecb-course-panel__disc{position:relative;z-index:2;display:flex;flex:0 0 auto;align-items:center;justify-content:center;border-radius:50%;background:#fff;border:none;color:#919191}.pecb-course-panel__disc svg{display:block}.pecb-course-panel__disc--plain{border:1.5px solid #ececec}.pecb-course-panel__disc--completed{background:#57bc57;color:#fff}.pecb-course-panel__disc--playing{background:var(--pecb-course-panel-accent, #212427);color:#fff;box-shadow:0 0 0 4px var(--pecb-course-panel-accent-soft, rgba(33, 36, 39, .27))}.pecb-course-panel__disc--started{background:#fff;color:#ff9c4a}.pecb-course-panel__disc--locked{color:#c2c2c2}.pecb-course-panel__ring{position:absolute;inset:0;transform:rotate(-90deg)}.pecb-course-panel__ring-track{stroke:#ececec}.pecb-course-panel__ring-value{stroke:#ff9c4a;transition:stroke-dashoffset .3s ease}.pecb-course-panel__disc-glyph{position:relative;display:flex;color:#ff9c4a}.pecb-course-panel__connector{flex:1 1 auto;width:2px;margin-top:2px;border-radius:2px;background:#ececec}.pecb-course-panel__connector--done{background:#57bc57}.pecb-course-panel__row-body{display:block;flex:1 1 auto;min-width:0}.pecb-course-panel__quiz-badge-line{display:flex;align-items:center;min-height:18px;margin-bottom:5px}.pecb-course-panel__quiz-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:9999px;background:#8e6df0;color:#fff;font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:9.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase}.pecb-course-panel__row-title{display:block;margin-bottom:4px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:var(--_title-size);font-weight:500;line-height:1.35;color:#212427;text-wrap:pretty;overflow-wrap:anywhere}.pecb-course-panel--compact .pecb-course-panel__row-title{margin-bottom:2px}.pecb-course-panel__row--active .pecb-course-panel__row-title{font-weight:600}.pecb-course-panel__row-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.pecb-course-panel__meta-primary{display:inline-flex;align-items:center;gap:5px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11.5px;font-weight:500;color:#919191}.pecb-course-panel__row--quiz .pecb-course-panel__meta-primary{color:#8e6df0}.pecb-course-panel__meta-result{display:inline-flex;align-items:center;gap:4px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11px;font-weight:600;color:#ff9c4a}.pecb-course-panel__meta-result--passed{color:#57bc57}.pecb-course-panel__meta-time{display:inline-flex;align-items:center;gap:5px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11px;font-weight:600;color:#ff9c4a}.pecb-course-panel__row--playing .pecb-course-panel__meta-time{color:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__bar{display:block;margin-top:7px;height:4px;border-radius:4px;background:#ececec;overflow:hidden}.pecb-course-panel__bar-fill{display:block;height:100%;border-radius:4px;background:#ff9c4a;transition:width .2s linear}.pecb-course-panel__row--playing .pecb-course-panel__bar-fill{background:var(--pecb-course-panel-accent, #212427)}.pecb-course-panel__timeline{padding:6px 14px 16px}.pecb-course-panel--compact .pecb-course-panel__timeline{padding:4px 12px 12px}.pecb-course-panel__timeline-head{display:flex;align-items:center;gap:10px;padding:18px 2px 8px}.pecb-course-panel--compact .pecb-course-panel__timeline-head{padding:14px 2px 6px}.pecb-course-panel__timeline-label{font-family:var(--pecb-course-panel-font-display, Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:#919191}.pecb-course-panel__timeline-rule{flex:1 1 auto;height:1px;background:var(--pecb-course-panel-border, #ececec)}.pecb-course-panel__timeline-count{font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:11px;color:#919191}.pecb-course-panel__transcript{padding:0 14px}.pecb-course-panel__cues{padding:8px 6px 20px}.pecb-course-panel__cue{border:none;background:none;font:inherit;color:inherit;display:flex;gap:14px;width:100%;padding:10px 12px;border-radius:8px;text-align:left;background:transparent;cursor:pointer;transition:background .14s ease}.pecb-course-panel__cue:focus{outline:none}.pecb-course-panel__cue:hover:not(.pecb-course-panel__cue--current){background:#fafafa}.pecb-course-panel__cue:focus-visible{outline:2px solid #a11e29;outline-offset:-2px}.pecb-course-panel__cue--current{background:#fbeaec}.pecb-course-panel__cue-time{flex:0 0 auto;width:38px;padding-top:1px;font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;color:#919191}.pecb-course-panel__cue--current .pecb-course-panel__cue-time{color:#a11e29}.pecb-course-panel__cue-text{font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13.5px;line-height:1.55;color:#313131;text-wrap:pretty;overflow-wrap:anywhere}.pecb-course-panel__cue--current .pecb-course-panel__cue-text{color:#212427}.pecb-course-panel__mark{padding:0 1px;border-radius:3px;background:#fce3b4;color:inherit}.pecb-course-panel__footer{flex:0 0 auto;padding:12px 20px;border-top:1px solid var(--pecb-course-panel-border, #ececec);background:var(--pecb-course-panel-bg, #ffffff)}.pecb-course-panel__autoscroll{display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.pecb-course-panel__autoscroll-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.pecb-course-panel__autoscroll-box{display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:18px;height:18px;border:1.5px solid #c2c2c2;border-radius:5px;background:#fff;color:#fff;transition:all .14s ease}.pecb-course-panel__autoscroll-input:checked+.pecb-course-panel__autoscroll-box{border:1px solid #a11e29;background:#a11e29}.pecb-course-panel__autoscroll-input:focus-visible+.pecb-course-panel__autoscroll-box{outline:2px solid #a11e29;outline-offset:2px}.pecb-course-panel__autoscroll-label{font-family:var(--pecb-course-panel-font-body, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);font-size:13px;font-weight:500;color:#616161}@container pecb-course-panel (max-width: 380px){.pecb-course-panel{--_row-pad-right: 12px}.pecb-course-panel__tabs{padding:0 12px}.pecb-course-panel__tab{padding:12px 0 10px;font-size:13px}.pecb-course-panel__search{padding:10px 12px 8px}.pecb-course-panel__progress{flex-wrap:wrap;gap:12px;padding:12px 16px}.pecb-course-panel__section-head{gap:10px;padding:12px}.pecb-course-panel__section-body{padding-right:8px}.pecb-course-panel__transcript{padding:0 8px}.pecb-course-panel__cue{gap:10px;padding:8px 10px}.pecb-course-panel__cue-text{font-size:13px}.pecb-course-panel__footer{padding:10px 16px}.pecb-course-panel__timeline{padding:4px 10px 12px}}@media (hover: none) and (pointer: coarse){.pecb-course-panel__row,.pecb-course-panel__cue{min-height:44px}}@media (prefers-reduced-motion: reduce){.pecb-course-panel *,.pecb-course-panel *:before,.pecb-course-panel *:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}\n"] }]
12273
+ }], ctorParameters: () => [], propDecorators: { transcriptScrollRef: [{ type: i0.ViewChild, args: ['transcriptScroll', { isSignal: true }] }], modules: [{ type: i0.Input, args: [{ isSignal: true, alias: "modules", required: false }] }], transcript: [{ type: i0.Input, args: [{ isSignal: true, alias: "transcript", required: false }] }], activeLessonId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeLessonId", required: false }] }, { type: i0.Output, args: ["activeLessonIdChange"] }], tab: [{ type: i0.Input, args: [{ isSignal: true, alias: "tab", required: false }] }, { type: i0.Output, args: ["tabChange"] }], openModuleId: [{ type: i0.Input, args: [{ isSignal: true, alias: "openModuleId", required: false }] }, { type: i0.Output, args: ["openModuleIdChange"] }], autoscroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoscroll", required: false }] }, { type: i0.Output, args: ["autoscrollChange"] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }], transcriptQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "transcriptQuery", required: false }] }, { type: i0.Output, args: ["transcriptQueryChange"] }], playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], accent: [{ type: i0.Input, args: [{ isSignal: true, alias: "accent", required: false }] }], showTabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTabs", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], showTranscriptSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTranscriptSearch", required: false }] }], showAutoscroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAutoscroll", required: false }] }], showProgress: [{ type: i0.Input, args: [{ isSignal: true, alias: "showProgress", required: false }] }], showCertificate: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCertificate", required: false }] }], soloSingleLessonModules: [{ type: i0.Input, args: [{ isSignal: true, alias: "soloSingleLessonModules", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], lessonSelect: [{ type: i0.Output, args: ["lessonSelect"] }], transcriptSeek: [{ type: i0.Output, args: ["transcriptSeek"] }], moduleToggle: [{ type: i0.Output, args: ["moduleToggle"] }], certificateClick: [{ type: i0.Output, args: ["certificateClick"] }] } });
12274
+ /** Replaces `{token}` placeholders in a label. */
12275
+ function format(template, values) {
12276
+ return template.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
12277
+ }
12278
+
12279
+ /**
12280
+ * PECB course player — the video stage with the **Content** sidebar attached,
12281
+ * exactly as laid out in "Course Player — Content Redesign".
12282
+ *
12283
+ * Composes {@link VideoPlayerComponent} and {@link CourseContentPanelComponent}
12284
+ * and wires them together: picking a lesson loads its media and resumes at its
12285
+ * saved position, clicking a transcript cue seeks the video, and the panel
12286
+ * follows playback live (now-playing row, progress bars, transcript highlight).
12287
+ *
12288
+ * The two-column grid collapses to a single stacked column when the space it is
12289
+ * given gets narrow, so the same markup works in a page, a drawer or a phone.
12290
+ *
12291
+ * @example
12292
+ * ```html
12293
+ * <pecb-course-player
12294
+ * [modules]="modules"
12295
+ * [(activeLessonId)]="lessonId"
12296
+ * (lessonSelect)="track($event)"
12297
+ * (lessonEnded)="markComplete($event)">
12298
+ * </pecb-course-player>
12299
+ * ```
12300
+ *
12301
+ * Layout hooks:
12302
+ * ```css
12303
+ * pecb-course-player {
12304
+ * --pecb-course-player-panel-width: 408px;
12305
+ * --pecb-course-player-gap: 20px;
12306
+ * --pecb-course-player-panel-stacked-height: 460px;
12307
+ * }
12308
+ * ```
12309
+ */
12310
+ class CoursePlayerComponent {
12311
+ constructor() {
12312
+ // ── Data ────────────────────────────────────────────────────────────────────
12313
+ /** Course structure. */
12314
+ this.modules = input([], ...(ngDevMode ? [{ debugName: "modules" }] : []));
12315
+ /** Fallback transcript, used when the active lesson has no `transcript`. */
12316
+ this.transcript = input([], ...(ngDevMode ? [{ debugName: "transcript" }] : []));
12317
+ // ── State (two-way) ─────────────────────────────────────────────────────────
12318
+ /** Selected lesson. */
12319
+ this.activeLessonId = model(null, ...(ngDevMode ? [{ debugName: "activeLessonId" }] : []));
12320
+ /** Playback state of the video. */
12321
+ this.playing = model(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
12322
+ /** Playhead position, in seconds. */
12323
+ this.currentTime = model(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
12324
+ /** Which panel tab is showing. */
12325
+ this.panelTab = model('content', ...(ngDevMode ? [{ debugName: "panelTab" }] : []));
12326
+ // ── Video options ───────────────────────────────────────────────────────────
12327
+ /** Poster used when the active lesson has none. */
12328
+ this.poster = input(undefined, ...(ngDevMode ? [{ debugName: "poster" }] : []));
12329
+ /** Media used when the active lesson has no `src` (e.g. a single-video course). */
12330
+ this.src = input(undefined, ...(ngDevMode ? [{ debugName: "src" }] : []));
12331
+ /** Caption tracks used when the active lesson has none. */
12332
+ this.tracks = input([], ...(ngDevMode ? [{ debugName: "tracks" }] : []));
12333
+ /** Stage label used when the active lesson has no `stageLabel`. */
12334
+ this.stageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "stageLabel" }] : []));
12335
+ /** Brand watermark on the video stage. @default 'PECB' */
12336
+ this.watermark = input('PECB', ...(ngDevMode ? [{ debugName: "watermark" }] : []));
12337
+ /** @default '16 / 9' */
12338
+ this.aspectRatio = input('16 / 9', ...(ngDevMode ? [{ debugName: "aspectRatio" }] : []));
12339
+ /** @default 'metadata' */
12340
+ this.preload = input('metadata', ...(ngDevMode ? [{ debugName: "preload" }] : []));
12341
+ /** `crossorigin` for the media element. */
12342
+ this.crossOrigin = input(undefined, ...(ngDevMode ? [{ debugName: "crossOrigin" }] : []));
12343
+ /** Seconds the rewind button / arrow keys jump. @default 10 */
12344
+ this.seekStep = input(10, ...(ngDevMode ? [{ debugName: "seekStep", transform: numberAttribute }] : [{ transform: numberAttribute }]));
12345
+ /** i18n overrides for the video player. */
12346
+ this.videoLabels = input({}, ...(ngDevMode ? [{ debugName: "videoLabels" }] : []));
12347
+ /** Start playing as soon as a video lesson is picked. @default true */
12348
+ this.autoPlayOnSelect = input(true, ...(ngDevMode ? [{ debugName: "autoPlayOnSelect", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12349
+ /** Show the active lesson's title under the video. @default true */
12350
+ this.showLessonTitle = input(true, ...(ngDevMode ? [{ debugName: "showLessonTitle", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12351
+ // ── Panel options ───────────────────────────────────────────────────────────
12352
+ /** Which side the panel sits on. @default 'end' */
12353
+ this.panelPosition = input('end', ...(ngDevMode ? [{ debugName: "panelPosition" }] : []));
12354
+ /** @default 'grouped' */
12355
+ this.layout = input('grouped', ...(ngDevMode ? [{ debugName: "layout" }] : []));
12356
+ /** @default 'comfortable' */
12357
+ this.density = input('comfortable', ...(ngDevMode ? [{ debugName: "density" }] : []));
12358
+ /** @default 'charcoal' */
12359
+ this.accent = input('charcoal', ...(ngDevMode ? [{ debugName: "accent" }] : []));
12360
+ /** Show the panel's tab strip. @default true */
12361
+ this.showTabs = input(true, ...(ngDevMode ? [{ debugName: "showTabs", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12362
+ /** Show the lesson search box. @default true */
12363
+ this.showSearch = input(true, ...(ngDevMode ? [{ debugName: "showSearch", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12364
+ /** Show the circular course-progress header. @default false */
12365
+ this.showProgress = input(false, ...(ngDevMode ? [{ debugName: "showProgress", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12366
+ /** Render single-lesson sections as direct-play rows. @default false */
12367
+ this.soloSingleLessonModules = input(false, ...(ngDevMode ? [{ debugName: "soloSingleLessonModules", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12368
+ /** i18n overrides for the panel. */
12369
+ this.panelLabels = input({}, ...(ngDevMode ? [{ debugName: "panelLabels" }] : []));
12370
+ // ── Outputs ─────────────────────────────────────────────────────────────────
12371
+ /** A lesson row was picked (fires for quizzes too). */
12372
+ this.lessonSelect = output();
12373
+ /** The video of a lesson played to the end. */
12374
+ this.lessonEnded = output();
12375
+ /** Playhead moved — use it to persist progress. */
12376
+ this.timeUpdate = output();
12377
+ /** The panel's "Certificate" pill was clicked. */
12378
+ this.certificateClick = output();
12379
+ /** The media failed to load, or playback was rejected. */
12380
+ this.playerError = output();
12381
+ // ── Derived ─────────────────────────────────────────────────────────────────
12382
+ /** The lesson currently selected in the panel (may be a quiz). */
12383
+ this.activeLesson = computed(() => findCourseLesson(this.modules(), this.activeLessonId()), ...(ngDevMode ? [{ debugName: "activeLesson" }] : []));
12384
+ /**
12385
+ * Id of the last *video* lesson that was selected. Picking a quiz keeps the
12386
+ * video on the stage (design behaviour) instead of blanking it.
12387
+ */
12388
+ this.videoLessonId = signal(null, ...(ngDevMode ? [{ debugName: "videoLessonId" }] : []));
12389
+ this.videoLesson = computed(() => {
12390
+ const explicit = findCourseLesson(this.modules(), this.videoLessonId());
12391
+ if (explicit)
12392
+ return explicit;
12393
+ // Fall back to the first video lesson so the stage is never empty.
12394
+ for (const module of this.modules()) {
12395
+ const lesson = module.lessons.find((l) => (l.type ?? 'video') === 'video');
12396
+ if (lesson)
12397
+ return lesson;
12398
+ }
12399
+ return undefined;
12400
+ }, ...(ngDevMode ? [{ debugName: "videoLesson" }] : []));
12401
+ this.videoSrc = computed(() => this.videoLesson()?.src ?? this.src(), ...(ngDevMode ? [{ debugName: "videoSrc" }] : []));
12402
+ this.videoPoster = computed(() => this.videoLesson()?.poster ?? this.poster(), ...(ngDevMode ? [{ debugName: "videoPoster" }] : []));
12403
+ this.videoTracks = computed(() => this.videoLesson()?.tracks ?? this.tracks(), ...(ngDevMode ? [{ debugName: "videoTracks" }] : []));
12404
+ this.videoStageLabel = computed(() => this.videoLesson()?.stageLabel ?? this.stageLabel(), ...(ngDevMode ? [{ debugName: "videoStageLabel" }] : []));
12405
+ this.videoTitle = computed(() => this.videoLesson()?.title, ...(ngDevMode ? [{ debugName: "videoTitle" }] : []));
12406
+ this.lessonTitle = computed(() => this.activeLesson()?.title ?? '', ...(ngDevMode ? [{ debugName: "lessonTitle" }] : []));
12407
+ /** Resume position handed to the player whenever the source changes. */
12408
+ this.startTime = computed(() => this.videoLesson()?.resumeSeconds ?? 0, ...(ngDevMode ? [{ debugName: "startTime" }] : []));
12409
+ this.hostClasses = computed(() => [
12410
+ 'pecb-course-player',
12411
+ `pecb-course-player--panel-${this.panelPosition()}`,
12412
+ ].join(' '), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
12413
+ /** Id of the lesson whose resume position has already been pushed into `currentTime`. */
12414
+ this.resumeAppliedFor = null;
12415
+ // Track the newest video lesson, including the initial selection.
12416
+ effect(() => {
12417
+ const lesson = this.activeLesson();
12418
+ untracked(() => {
12419
+ if (lesson && (lesson.type ?? 'video') === 'video') {
12420
+ this.videoLessonId.set(lesson.id);
12421
+ }
12422
+ });
12423
+ });
12424
+ // Seed the playhead from the lesson's stored position whenever the loaded
12425
+ // video changes — including the very first one, which arrives as an input
12426
+ // rather than through a click. Without this the panel would show 0:00 for a
12427
+ // half-watched lesson until the media reported its own time.
12428
+ effect(() => {
12429
+ const lesson = this.videoLesson();
12430
+ untracked(() => {
12431
+ if (!lesson || lesson.id === this.resumeAppliedFor)
12432
+ return;
12433
+ this.resumeAppliedFor = lesson.id;
12434
+ this.currentTime.set(lesson.resumeSeconds ?? 0);
12435
+ });
12436
+ });
12437
+ }
12438
+ // ── Wiring ──────────────────────────────────────────────────────────────────
12439
+ /** Panel → player: load a lesson, resuming where it left off. */
12440
+ onLessonSelect(lesson) {
12441
+ const isVideo = (lesson.type ?? 'video') === 'video';
12442
+ if (isVideo) {
12443
+ this.videoLessonId.set(lesson.id);
12444
+ this.currentTime.set(lesson.resumeSeconds ?? 0);
12445
+ this.playing.set(this.autoPlayOnSelect());
12446
+ }
12447
+ else {
12448
+ // A quiz has nothing to play — stop the video and leave it on the stage.
12449
+ this.playing.set(false);
12450
+ }
12451
+ this.lessonSelect.emit(lesson);
12452
+ }
12453
+ /** Panel → player: jump to a transcript cue and keep playing. */
12454
+ onTranscriptSeek(seconds) {
12455
+ this.currentTime.set(seconds);
12456
+ this.playing.set(true);
12457
+ }
12458
+ onPlaybackEnded() {
12459
+ const lesson = this.videoLesson();
12460
+ if (lesson)
12461
+ this.lessonEnded.emit(lesson);
12462
+ }
12463
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CoursePlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
12464
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: CoursePlayerComponent, isStandalone: true, selector: "pecb-course-player", inputs: { modules: { classPropertyName: "modules", publicName: "modules", isSignal: true, isRequired: false, transformFunction: null }, transcript: { classPropertyName: "transcript", publicName: "transcript", isSignal: true, isRequired: false, transformFunction: null }, activeLessonId: { classPropertyName: "activeLessonId", publicName: "activeLessonId", isSignal: true, isRequired: false, transformFunction: null }, playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, panelTab: { classPropertyName: "panelTab", publicName: "panelTab", isSignal: true, isRequired: false, transformFunction: null }, poster: { classPropertyName: "poster", publicName: "poster", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, tracks: { classPropertyName: "tracks", publicName: "tracks", isSignal: true, isRequired: false, transformFunction: null }, stageLabel: { classPropertyName: "stageLabel", publicName: "stageLabel", isSignal: true, isRequired: false, transformFunction: null }, watermark: { classPropertyName: "watermark", publicName: "watermark", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, preload: { classPropertyName: "preload", publicName: "preload", isSignal: true, isRequired: false, transformFunction: null }, crossOrigin: { classPropertyName: "crossOrigin", publicName: "crossOrigin", isSignal: true, isRequired: false, transformFunction: null }, seekStep: { classPropertyName: "seekStep", publicName: "seekStep", isSignal: true, isRequired: false, transformFunction: null }, videoLabels: { classPropertyName: "videoLabels", publicName: "videoLabels", isSignal: true, isRequired: false, transformFunction: null }, autoPlayOnSelect: { classPropertyName: "autoPlayOnSelect", publicName: "autoPlayOnSelect", isSignal: true, isRequired: false, transformFunction: null }, showLessonTitle: { classPropertyName: "showLessonTitle", publicName: "showLessonTitle", isSignal: true, isRequired: false, transformFunction: null }, panelPosition: { classPropertyName: "panelPosition", publicName: "panelPosition", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, showTabs: { classPropertyName: "showTabs", publicName: "showTabs", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, showProgress: { classPropertyName: "showProgress", publicName: "showProgress", isSignal: true, isRequired: false, transformFunction: null }, soloSingleLessonModules: { classPropertyName: "soloSingleLessonModules", publicName: "soloSingleLessonModules", isSignal: true, isRequired: false, transformFunction: null }, panelLabels: { classPropertyName: "panelLabels", publicName: "panelLabels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeLessonId: "activeLessonIdChange", playing: "playingChange", currentTime: "currentTimeChange", panelTab: "panelTabChange", lessonSelect: "lessonSelect", lessonEnded: "lessonEnded", timeUpdate: "timeUpdate", certificateClick: "certificateClick", playerError: "playerError" }, host: { classAttribute: "pecb-course-player-host" }, ngImport: i0, template: "<div [class]=\"hostClasses()\">\n <!-- \u2500\u2500 Video stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-course-player__stage\">\n <pecb-video-player\n class=\"pecb-course-player__video\"\n [src]=\"videoSrc()\"\n [poster]=\"videoPoster()\"\n [tracks]=\"videoTracks()\"\n [title]=\"videoTitle()\"\n [label]=\"videoStageLabel()\"\n [watermark]=\"watermark()\"\n [aspectRatio]=\"aspectRatio()\"\n [preload]=\"preload()\"\n [crossOrigin]=\"crossOrigin()\"\n [seekStep]=\"seekStep()\"\n [startTime]=\"startTime()\"\n [labels]=\"videoLabels()\"\n [(playing)]=\"playing\"\n [(currentTime)]=\"currentTime\"\n (timeUpdate)=\"timeUpdate.emit($event)\"\n (playbackEnded)=\"onPlaybackEnded()\"\n (playerError)=\"playerError.emit($event)\"\n ></pecb-video-player>\n\n @if (showLessonTitle() && lessonTitle()) {\n <h2 class=\"pecb-course-player__title\">{{ lessonTitle() }}</h2>\n }\n </div>\n\n <!-- \u2500\u2500 Content sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-course-player__panel\">\n <div class=\"pecb-course-player__panel-inner\">\n <pecb-course-content-panel\n [modules]=\"modules()\"\n [transcript]=\"transcript()\"\n [(activeLessonId)]=\"activeLessonId\"\n [(tab)]=\"panelTab\"\n [playing]=\"playing()\"\n [currentTime]=\"currentTime()\"\n [layout]=\"layout()\"\n [density]=\"density()\"\n [accent]=\"accent()\"\n [showTabs]=\"showTabs()\"\n [showSearch]=\"showSearch()\"\n [showProgress]=\"showProgress()\"\n [soloSingleLessonModules]=\"soloSingleLessonModules()\"\n [labels]=\"panelLabels()\"\n (lessonSelect)=\"onLessonSelect($event)\"\n (transcriptSeek)=\"onTranscriptSeek($event)\"\n (certificateClick)=\"certificateClick.emit()\"\n ></pecb-course-content-panel>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0;container-type:inline-size;container-name:pecb-course-player}.pecb-course-player{display:grid;grid-template-columns:minmax(0,1fr) var(--pecb-course-player-panel-width, 408px);gap:var(--pecb-course-player-gap, 20px);align-items:stretch;height:100%;min-height:0}.pecb-course-player--panel-start{grid-template-columns:var(--pecb-course-player-panel-width, 408px) minmax(0,1fr)}.pecb-course-player--panel-start .pecb-course-player__stage{order:2}.pecb-course-player--panel-start .pecb-course-player__panel{order:1}.pecb-course-player__stage{display:flex;flex-direction:column;gap:14px;min-width:0;min-height:0}.pecb-course-player__video{flex:0 0 auto}.pecb-course-player__title{flex:0 0 auto;margin:0;padding:0 2px;font-family:Plus Jakarta Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:16px;font-weight:700;line-height:1.35;color:#212427;text-wrap:pretty}.pecb-course-player__panel{position:relative;min-width:0;min-height:240px}.pecb-course-player__panel-inner{position:absolute;inset:0;display:flex;overflow:hidden;border:1px solid #ececec;border-radius:var(--pecb-course-player-radius, 12px);background:#fff}.pecb-course-player pecb-course-content-panel{flex:1 1 auto;min-width:0}@container pecb-course-player (max-width: 900px){.pecb-course-player,.pecb-course-player--panel-start{grid-template-columns:minmax(0,1fr);height:auto}.pecb-course-player__stage,.pecb-course-player--panel-start .pecb-course-player__stage{order:1}.pecb-course-player__panel,.pecb-course-player--panel-start .pecb-course-player__panel{order:2;position:static;min-height:0;height:var(--pecb-course-player-panel-stacked-height, 460px)}.pecb-course-player__panel-inner{position:static;height:100%}}@container pecb-course-player (max-width: 560px){.pecb-course-player{gap:16px}.pecb-course-player__panel{height:var(--pecb-course-player-panel-stacked-height, 400px)}.pecb-course-player__title{font-size:14px}}\n"], dependencies: [{ kind: "component", type: VideoPlayerComponent, selector: "pecb-video-player", inputs: ["src", "poster", "tracks", "title", "crossOrigin", "preload", "autoplay", "loop", "playsInline", "startTime", "watermark", "label", "aspectRatio", "showCenterButton", "showSkipBack", "showVolume", "showTime", "showCaptions", "showSettings", "showFullscreen", "seekStep", "playbackRates", "labels", "playing", "currentTime", "volume", "muted", "playbackRate", "captionsEnabled"], outputs: ["playingChange", "currentTimeChange", "volumeChange", "mutedChange", "playbackRateChange", "captionsEnabledChange", "timeUpdate", "loadedMetadata", "seekEnd", "playbackEnded", "fullscreenChange", "playerError"] }, { kind: "component", type: CourseContentPanelComponent, selector: "pecb-course-content-panel", inputs: ["modules", "transcript", "activeLessonId", "tab", "openModuleId", "autoscroll", "query", "transcriptQuery", "playing", "currentTime", "layout", "density", "accent", "showTabs", "showSearch", "showTranscriptSearch", "showAutoscroll", "showProgress", "showCertificate", "soloSingleLessonModules", "labels"], outputs: ["activeLessonIdChange", "tabChange", "openModuleIdChange", "autoscrollChange", "queryChange", "transcriptQueryChange", "lessonSelect", "transcriptSeek", "moduleToggle", "certificateClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12465
+ }
12466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: CoursePlayerComponent, decorators: [{
12467
+ type: Component,
12468
+ args: [{ selector: 'pecb-course-player', standalone: true, imports: [VideoPlayerComponent, CourseContentPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'pecb-course-player-host' }, template: "<div [class]=\"hostClasses()\">\n <!-- \u2500\u2500 Video stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-course-player__stage\">\n <pecb-video-player\n class=\"pecb-course-player__video\"\n [src]=\"videoSrc()\"\n [poster]=\"videoPoster()\"\n [tracks]=\"videoTracks()\"\n [title]=\"videoTitle()\"\n [label]=\"videoStageLabel()\"\n [watermark]=\"watermark()\"\n [aspectRatio]=\"aspectRatio()\"\n [preload]=\"preload()\"\n [crossOrigin]=\"crossOrigin()\"\n [seekStep]=\"seekStep()\"\n [startTime]=\"startTime()\"\n [labels]=\"videoLabels()\"\n [(playing)]=\"playing\"\n [(currentTime)]=\"currentTime\"\n (timeUpdate)=\"timeUpdate.emit($event)\"\n (playbackEnded)=\"onPlaybackEnded()\"\n (playerError)=\"playerError.emit($event)\"\n ></pecb-video-player>\n\n @if (showLessonTitle() && lessonTitle()) {\n <h2 class=\"pecb-course-player__title\">{{ lessonTitle() }}</h2>\n }\n </div>\n\n <!-- \u2500\u2500 Content sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-course-player__panel\">\n <div class=\"pecb-course-player__panel-inner\">\n <pecb-course-content-panel\n [modules]=\"modules()\"\n [transcript]=\"transcript()\"\n [(activeLessonId)]=\"activeLessonId\"\n [(tab)]=\"panelTab\"\n [playing]=\"playing()\"\n [currentTime]=\"currentTime()\"\n [layout]=\"layout()\"\n [density]=\"density()\"\n [accent]=\"accent()\"\n [showTabs]=\"showTabs()\"\n [showSearch]=\"showSearch()\"\n [showProgress]=\"showProgress()\"\n [soloSingleLessonModules]=\"soloSingleLessonModules()\"\n [labels]=\"panelLabels()\"\n (lessonSelect)=\"onLessonSelect($event)\"\n (transcriptSeek)=\"onTranscriptSeek($event)\"\n (certificateClick)=\"certificateClick.emit()\"\n ></pecb-course-content-panel>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0;container-type:inline-size;container-name:pecb-course-player}.pecb-course-player{display:grid;grid-template-columns:minmax(0,1fr) var(--pecb-course-player-panel-width, 408px);gap:var(--pecb-course-player-gap, 20px);align-items:stretch;height:100%;min-height:0}.pecb-course-player--panel-start{grid-template-columns:var(--pecb-course-player-panel-width, 408px) minmax(0,1fr)}.pecb-course-player--panel-start .pecb-course-player__stage{order:2}.pecb-course-player--panel-start .pecb-course-player__panel{order:1}.pecb-course-player__stage{display:flex;flex-direction:column;gap:14px;min-width:0;min-height:0}.pecb-course-player__video{flex:0 0 auto}.pecb-course-player__title{flex:0 0 auto;margin:0;padding:0 2px;font-family:Plus Jakarta Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:16px;font-weight:700;line-height:1.35;color:#212427;text-wrap:pretty}.pecb-course-player__panel{position:relative;min-width:0;min-height:240px}.pecb-course-player__panel-inner{position:absolute;inset:0;display:flex;overflow:hidden;border:1px solid #ececec;border-radius:var(--pecb-course-player-radius, 12px);background:#fff}.pecb-course-player pecb-course-content-panel{flex:1 1 auto;min-width:0}@container pecb-course-player (max-width: 900px){.pecb-course-player,.pecb-course-player--panel-start{grid-template-columns:minmax(0,1fr);height:auto}.pecb-course-player__stage,.pecb-course-player--panel-start .pecb-course-player__stage{order:1}.pecb-course-player__panel,.pecb-course-player--panel-start .pecb-course-player__panel{order:2;position:static;min-height:0;height:var(--pecb-course-player-panel-stacked-height, 460px)}.pecb-course-player__panel-inner{position:static;height:100%}}@container pecb-course-player (max-width: 560px){.pecb-course-player{gap:16px}.pecb-course-player__panel{height:var(--pecb-course-player-panel-stacked-height, 400px)}.pecb-course-player__title{font-size:14px}}\n"] }]
12469
+ }], ctorParameters: () => [], propDecorators: { modules: [{ type: i0.Input, args: [{ isSignal: true, alias: "modules", required: false }] }], transcript: [{ type: i0.Input, args: [{ isSignal: true, alias: "transcript", required: false }] }], activeLessonId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeLessonId", required: false }] }, { type: i0.Output, args: ["activeLessonIdChange"] }], playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }, { type: i0.Output, args: ["playingChange"] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }, { type: i0.Output, args: ["currentTimeChange"] }], panelTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelTab", required: false }] }, { type: i0.Output, args: ["panelTabChange"] }], poster: [{ type: i0.Input, args: [{ isSignal: true, alias: "poster", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], tracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "tracks", required: false }] }], stageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "stageLabel", required: false }] }], watermark: [{ type: i0.Input, args: [{ isSignal: true, alias: "watermark", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], preload: [{ type: i0.Input, args: [{ isSignal: true, alias: "preload", required: false }] }], crossOrigin: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossOrigin", required: false }] }], seekStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "seekStep", required: false }] }], videoLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoLabels", required: false }] }], autoPlayOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoPlayOnSelect", required: false }] }], showLessonTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLessonTitle", required: false }] }], panelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelPosition", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], accent: [{ type: i0.Input, args: [{ isSignal: true, alias: "accent", required: false }] }], showTabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTabs", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], showProgress: [{ type: i0.Input, args: [{ isSignal: true, alias: "showProgress", required: false }] }], soloSingleLessonModules: [{ type: i0.Input, args: [{ isSignal: true, alias: "soloSingleLessonModules", required: false }] }], panelLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelLabels", required: false }] }], lessonSelect: [{ type: i0.Output, args: ["lessonSelect"] }], lessonEnded: [{ type: i0.Output, args: ["lessonEnded"] }], timeUpdate: [{ type: i0.Output, args: ["timeUpdate"] }], certificateClick: [{ type: i0.Output, args: ["certificateClick"] }], playerError: [{ type: i0.Output, args: ["playerError"] }] } });
12470
+
10658
12471
  /**
10659
12472
  * An injectable, Toastr-style notification service.
10660
12473
  *
@@ -17297,5 +19110,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
17297
19110
  * Generated bundle index. Do not edit.
17298
19111
  */
17299
19112
 
17300
- export { AccordionItemComponent, AccordionSmallComponent, AddButtonComponent, AdminHeaderComponent, AdminHeaderFieldTemplateDirective, AffixComponent, AlertComponent, AnchorComponent, ApplicationStatusBarComponent, AuditorStatusComponent, AuthorDateTimeComponent, BackToTopComponent, BadgeComponent, BlurDirective, BottomSheetComponent, BreadcrumbsComponent, ButtonComponent, ButtonGroupComponent, ButtonGroupItemComponent, CancelUpdateButtonsComponent, CardBodyComponent, CardComponent, CardFooterComponent, CardHeaderComponent, CertificateUploadBarComponent, CheckDeleteIconComponent, CheckboxComponent, CheckboxDisplayComponent, CodeInputComponent, CodeSnippetComponent, ColorPaletteComponent, ConfirmationComponent, ContentTypeTagComponent, DEFAULT_LANGUAGES, DashboardGridComponent, DatepickerComponent, DividerComponent, DropdownComponent, EMPTY_STATE_MAX_BUTTONS, EditButtonComponent, EditCoverPhotoComponent, EmptyStateComponent, ExpandableRowTableComponent, FileUploadComponent, FilterColumnsComponent, FloatButtonComponent, FloatButtonItemComponent, FullscreenModalComponent, FullscreenModalContentDirective, FullscreenModalFooterDirective, FullscreenModalHeaderDirective, GeneralComponent, GridComponent, GridItemComponent, HeaderActionsComponent, HeaderComponent, HeaderDividerComponent, HeaderLanguageComponent, HeaderSearchComponent, HeaderUserComponent, HierarchicalTableComponent, HorizontalStepsComponent, IconComponent, IconRegistry, IconTagComponent, InformationBoxComponent, InputComponent, LanguageDropdownComponent, LinkButtonComponent, LoadingService, MediaComponent, MessageBubbleComponent, MessageItemComponent, MetricsCardComponent, MiniIconButtonComponent, NoResultsComponent, NoteSidebarItemComponent, NotesPanelComponent, NotificationService, NotificationStatusLinkComponent, PECB_COLOR_PALETTE, PECB_CUSTOM_ICONS, PECB_FONT_STYLES, PECB_ICONS, PECB_TYPE_SCALE, PaginationComponent, PriceMethodComponent, ProfileComponent, ProfileElementsCardComponent, ProfileGroupComponent, ProgressBarComponent, ProgressCircleComponent, ProjectLayoutComponent, QuantitySelectorComponent, QuestionTypeTagComponent, RadioComponent, RadioDisplayComponent, RatingNumberComponent, ReasonForReturnComponent, RequestSentByComponent, RequestStatusBarComponent, ResultPageComponent, RightModalComponent, RightModalContentDirective, RightModalFooterDirective, RightModalHeaderDirective, ShadowDirective, SidebarComponent, SkeletonComponent, SlidePointsComponent, SpacerComponent, SpinnerComponent, StandardsPdfCardComponent, StatisticsCardComponent, StatusComponent, StepperComponent, TabComponent, TableComponent, TagComponent, TestimonialComponent, TextFormFieldComponent, ThemeService, ToggleComponent, ToolbarBarComponent, TooltipComponent, TooltipDirective, TourComponent, TranscriptLineComponent, TypographyComponent, UserWithEmailComponent, VerifyChecklistComponent, VideoUploadBarComponent, VirtualTableComponent, addClass, announceToScreenReader, capitalize, closestElement, copyToClipboard, createAuthError, createAuthorizationError, createConfigError, createError, createNetworkError, createValidationError, disableBodyScroll, escapeHtml, formatCount, formatErrorForLog, formatErrorMessage, generateLinkedIds, generateUniqueId, getAriaCurrent, getButtonAriaAttributes, getComputedStyleValue, getDialogAriaAttributes, getFocusableElements, getInitials, getInputAriaAttributes, getOptionAriaAttributes, getProgressAriaAttributes, getScrollParent, getTabAriaAttributes, getVisuallyHiddenStyles, handleError, hasClass, isBlank, isBrowser, isElementVisible, isNotBlank, isPecbError, isRecoverableError, matchesSelector, pluralize, prefersHighContrast, prefersReducedMotion, registerErrorHandler, removeClass, scrollIntoView, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toggleClass, trapFocus, truncate, tryAsync, trySync, wrapError };
19113
+ export { AccordionItemComponent, AccordionSmallComponent, AddButtonComponent, AdminHeaderComponent, AdminHeaderFieldTemplateDirective, AffixComponent, AlertComponent, AnchorComponent, ApplicationStatusBarComponent, AuditorStatusComponent, AuthorDateTimeComponent, BackToTopComponent, BadgeComponent, BlurDirective, BottomSheetComponent, BreadcrumbsComponent, ButtonComponent, ButtonGroupComponent, ButtonGroupItemComponent, CancelUpdateButtonsComponent, CardBodyComponent, CardComponent, CardFooterComponent, CardHeaderComponent, CertificateUploadBarComponent, CheckDeleteIconComponent, CheckboxComponent, CheckboxDisplayComponent, CodeInputComponent, CodeSnippetComponent, ColorPaletteComponent, ConfirmationComponent, ContentTypeTagComponent, CourseContentPanelComponent, CoursePlayerComponent, DEFAULT_COURSE_CONTENT_PANEL_LABELS, DEFAULT_LANGUAGES, DEFAULT_VIDEO_PLAYER_LABELS, DEFAULT_VIDEO_PLAYER_RATES, DashboardGridComponent, DatepickerComponent, DividerComponent, DropdownComponent, EMPTY_STATE_MAX_BUTTONS, EditButtonComponent, EditCoverPhotoComponent, EmptyStateComponent, ExpandableRowTableComponent, FileUploadComponent, FilterColumnsComponent, FloatButtonComponent, FloatButtonItemComponent, FullscreenModalComponent, FullscreenModalContentDirective, FullscreenModalFooterDirective, FullscreenModalHeaderDirective, GeneralComponent, GridComponent, GridItemComponent, HeaderActionsComponent, HeaderComponent, HeaderDividerComponent, HeaderLanguageComponent, HeaderSearchComponent, HeaderUserComponent, HierarchicalTableComponent, HorizontalStepsComponent, IconComponent, IconRegistry, IconTagComponent, InformationBoxComponent, InputComponent, LanguageDropdownComponent, LinkButtonComponent, LoadingService, MediaComponent, MessageBubbleComponent, MessageItemComponent, MetricsCardComponent, MiniIconButtonComponent, NoResultsComponent, NoteSidebarItemComponent, NotesPanelComponent, NotificationService, NotificationStatusLinkComponent, PECB_COLOR_PALETTE, PECB_CUSTOM_ICONS, PECB_FONT_STYLES, PECB_ICONS, PECB_TYPE_SCALE, PaginationComponent, PriceMethodComponent, ProfileComponent, ProfileElementsCardComponent, ProfileGroupComponent, ProgressBarComponent, ProgressCircleComponent, ProjectLayoutComponent, QuantitySelectorComponent, QuestionTypeTagComponent, RadioComponent, RadioDisplayComponent, RatingNumberComponent, ReasonForReturnComponent, RequestSentByComponent, RequestStatusBarComponent, ResultPageComponent, RightModalComponent, RightModalContentDirective, RightModalFooterDirective, RightModalHeaderDirective, ShadowDirective, SidebarComponent, SkeletonComponent, SlidePointsComponent, SpacerComponent, SpinnerComponent, StandardsPdfCardComponent, StatisticsCardComponent, StatusComponent, StepperComponent, TabComponent, TableComponent, TagComponent, TestimonialComponent, TextFormFieldComponent, ThemeService, ToggleComponent, ToolbarBarComponent, TooltipComponent, TooltipDirective, TourComponent, TranscriptLineComponent, TypographyComponent, UserWithEmailComponent, VerifyChecklistComponent, VideoPlayerComponent, VideoUploadBarComponent, VirtualTableComponent, addClass, announceToScreenReader, capitalize, closestElement, copyToClipboard, courseModuleStats, courseProgress, createAuthError, createAuthorizationError, createConfigError, createError, createNetworkError, createValidationError, disableBodyScroll, escapeHtml, findCourseLesson, findCourseModule, formatCount, formatCourseDuration, formatCourseTime, formatErrorForLog, formatErrorMessage, formatVideoTime, generateLinkedIds, generateUniqueId, getAriaCurrent, getButtonAriaAttributes, getComputedStyleValue, getDialogAriaAttributes, getFocusableElements, getInitials, getInputAriaAttributes, getOptionAriaAttributes, getProgressAriaAttributes, getScrollParent, getTabAriaAttributes, getVisuallyHiddenStyles, handleError, hasClass, isBlank, isBrowser, isElementVisible, isNotBlank, isPecbError, isRecoverableError, matchesSelector, parseLessonPartTitle, pluralize, prefersHighContrast, prefersReducedMotion, registerErrorHandler, removeClass, resolveLessonStatus, scrollIntoView, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toggleClass, trapFocus, truncate, tryAsync, trySync, wrapError };
17301
19114
  //# sourceMappingURL=pecb-ui-components.mjs.map