@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.
- package/README.md +257 -70
- package/data-display/index.d.ts +1 -1
- package/fesm2022/pecb-ui-components-data-display.mjs +2 -2
- package/fesm2022/pecb-ui-components-data-display.mjs.map +1 -1
- package/fesm2022/pecb-ui-components.mjs +1818 -5
- package/fesm2022/pecb-ui-components.mjs.map +1 -1
- package/index.d.ts +834 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|