@lifecorner/angular-ui-kit 3.25.1 → 3.27.0
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/fesm2022/lifecorner-angular-ui-kit-button.mjs +18 -2
- package/fesm2022/lifecorner-angular-ui-kit-button.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-callout.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-callout.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-chat.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-chat.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-color-picker.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-color-picker.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-date-range-picker.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-date-range-picker.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-dependency-viewer.mjs +47 -9
- package/fesm2022/lifecorner-angular-ui-kit-dependency-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-description-list.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-description-list.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs +8 -4
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-filter-bar.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-filter-bar.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gantt-chart.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-gantt-chart.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs +3 -2
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-list.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-list.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-number-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-number-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-scatter-plot.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-scatter-plot.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-search-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-search-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-select.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-select.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-split-pane.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-split-pane.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs +3 -2
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-tabs.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-tabs.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-toggle-group.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-toggle-group.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/_reset.scss +4 -2
- package/src/styles/focus-indicators.spec.ts +199 -0
- package/types/lifecorner-angular-ui-kit-button.d.ts +20 -0
- package/types/lifecorner-angular-ui-kit-dependency-viewer.d.ts +33 -4
- package/types/lifecorner-angular-ui-kit-description-list.d.ts +4 -0
- package/types/lifecorner-angular-ui-kit-file-upload.d.ts +11 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-filter-bar.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/filter-bar/filter-bar.component.ts","../../../../libs/angular-ui-kit/src/lib/filter-bar/filter-bar.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n computed,\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { SearchInputComponent } from '@lifecorner/angular-ui-kit/search-input';\nimport { SelectComponent, SelectOption } from '@lifecorner/angular-ui-kit/select';\n\n/**\n * Configuration for a single filter in the FilterBar.\n */\nexport interface FilterConfig {\n /** Unique key for this filter */\n readonly key: string;\n /** Display label */\n readonly label: string;\n /** Filter type */\n readonly type: 'select' | 'toggle' | 'search';\n /** Available options for select/toggle types */\n readonly options?: readonly FilterOption[];\n /** Placeholder text (for search type) */\n readonly placeholder?: string;\n}\n\nexport interface FilterOption {\n readonly value: string;\n readonly label: string;\n readonly disabled?: boolean;\n}\n\nexport interface FilterValues {\n [key: string]: string;\n}\n\nconst NO_OPTIONS: SelectOption[] = [];\n\n/**\n * Filter bar component for composable data filtering.\n *\n * Features:\n * - Select dropdowns, toggle groups, and search inputs\n * - Declarative filter configuration via FilterConfig array\n * - Two-way filter values binding\n * - Responsive horizontal flex layout\n * - Size variants (sm, md)\n * - Dark mode support\n *\n * @example\n * ```html\n * <lc-filter-bar\n * [filters]=\"filterConfig\"\n * [values]=\"currentFilters\"\n * (valuesChange)=\"onFilterChange($event)\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-filter-bar',\n standalone: true,\n imports: [FormsModule, SelectComponent, SearchInputComponent],\n templateUrl: './filter-bar.component.html',\n styleUrl: './filter-bar.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FilterBarComponent {\n private static nextId = 0;\n\n /** Per-instance id prefix for label/control linking */\n readonly barId = `lc-filter-bar-${++FilterBarComponent.nextId}`;\n\n /** Filter configurations */\n filters = input.required<readonly FilterConfig[]>();\n\n /** Current filter values (two-way bindable) */\n values = input<FilterValues>({});\n\n /** Size variant */\n size = input<'sm' | 'md'>('md');\n\n /** Placeholder of select filters without their own `placeholder`. */\n selectPlaceholder = input('Select an option');\n\n /** Placeholder of search filters without their own `placeholder`. */\n searchPlaceholder = input('Search...');\n\n /** Emitted when any filter value changes */\n valuesChange = output<FilterValues>();\n\n /** Computed size class */\n sizeClass = computed(() => `lc-filter-bar--${this.size()}`);\n\n /**\n * Select options per filter key, built once per `filters` change so the\n * lc-select `options` input keeps a stable reference across change detection.\n */\n private readonly selectOptionsByKey = computed(() => {\n const map = new Map<string, SelectOption[]>();\n for (const filter of this.filters()) {\n if (filter.type === 'select') {\n map.set(filter.key, (filter.options ?? []).map((opt) => ({ ...opt })));\n }\n }\n return map;\n });\n\n /** Get the current value for a filter key */\n getValue(key: string): string {\n return this.values()[key] ?? '';\n }\n\n /** Memoised select options for a filter key */\n selectOptions(key: string): SelectOption[] {\n return this.selectOptionsByKey().get(key) ?? NO_OPTIONS;\n }\n\n /** id of the control rendered for a filter (used by `<label for>`) */\n fieldId(key: string): string {\n return `${this.barId}-${key}`;\n }\n\n /** id of the label rendered for a filter (used by `aria-labelledby`) */\n labelId(key: string): string {\n return `${this.barId}-${key}-label`;\n }\n\n /** Handle select / toggle change */\n onFilterChange(key: string, value: string): void {\n const updated = { ...this.values(), [key]: value };\n this.valuesChange.emit(updated);\n }\n\n /** Handle search input */\n onSearchInput(key: string, value: string): void {\n this.onFilterChange(key, value);\n }\n\n normalizeSelectValue(value: string | number | null): string {\n return value == null ? '' : String(value);\n }\n\n /** Check if a toggle option is active */\n isToggleActive(key: string, optionValue: string): boolean {\n return this.getValue(key) === optionValue;\n }\n\n /** Get toggle button classes */\n getToggleClasses(key: string, option: FilterOption): string {\n const classes = ['lc-filter-bar__toggle-btn'];\n if (this.isToggleActive(key, option.value)) {\n classes.push('lc-filter-bar__toggle-btn--active');\n }\n if (option.disabled) {\n classes.push('lc-filter-bar__toggle-btn--disabled');\n }\n return classes.join(' ');\n }\n\n}\n","<div class=\"lc-filter-bar\" [class]=\"sizeClass()\">\n @for (filter of filters(); track filter.key) {\n <div class=\"lc-filter-bar__item\">\n @if (filter.label) {\n @if (filter.type === 'search') {\n <label class=\"lc-filter-bar__label\" [id]=\"labelId(filter.key)\" [for]=\"fieldId(filter.key)\">\n {{ filter.label }}\n </label>\n } @else {\n <span class=\"lc-filter-bar__label\" [id]=\"labelId(filter.key)\">{{ filter.label }}</span>\n }\n }\n\n @switch (filter.type) {\n @case ('select') {\n <lc-select\n class=\"lc-filter-bar__select\"\n [size]=\"size() === 'sm' ? 'sm' : 'md'\"\n [placeholder]=\"filter.placeholder ?? selectPlaceholder()\"\n [options]=\"selectOptions(filter.key)\"\n [ariaLabel]=\"filter.label || filter.placeholder || filter.key\"\n [ngModel]=\"getValue(filter.key)\"\n (ngModelChange)=\"onFilterChange(filter.key, normalizeSelectValue($event))\"\n />\n }\n\n @case ('toggle') {\n <div\n class=\"lc-filter-bar__toggle-group\"\n role=\"group\"\n [attr.aria-labelledby]=\"filter.label ? labelId(filter.key) : null\"\n [attr.aria-label]=\"filter.label ? null : filter.key\"\n >\n @for (opt of filter.options; track opt.value) {\n <button\n type=\"button\"\n [class]=\"getToggleClasses(filter.key, opt)\"\n [disabled]=\"opt.disabled ?? false\"\n [attr.aria-pressed]=\"isToggleActive(filter.key, opt.value)\"\n (click)=\"onFilterChange(filter.key, opt.value)\"\n >\n {{ opt.label }}\n </button>\n }\n </div>\n }\n\n @case ('search') {\n <lc-search-input\n class=\"lc-filter-bar__search\"\n [inputId]=\"fieldId(filter.key)\"\n [ariaLabelledBy]=\"filter.label ? labelId(filter.key) : undefined\"\n [ariaLabel]=\"filter.label ? undefined : filter.placeholder || filter.key\"\n [placeholder]=\"filter.placeholder ?? searchPlaceholder()\"\n [size]=\"size() === 'sm' ? 'sm' : 'md'\"\n [debounceMs]=\"0\"\n [value]=\"getValue(filter.key)\"\n (searchChange)=\"onSearchInput(filter.key, $event)\"\n />\n }\n }\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-filter-bar.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/filter-bar/filter-bar.component.ts","../../../../libs/angular-ui-kit/src/lib/filter-bar/filter-bar.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n computed,\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { SearchInputComponent } from '@lifecorner/angular-ui-kit/search-input';\nimport { SelectComponent, SelectOption } from '@lifecorner/angular-ui-kit/select';\n\n/**\n * Configuration for a single filter in the FilterBar.\n */\nexport interface FilterConfig {\n /** Unique key for this filter */\n readonly key: string;\n /** Display label */\n readonly label: string;\n /** Filter type */\n readonly type: 'select' | 'toggle' | 'search';\n /** Available options for select/toggle types */\n readonly options?: readonly FilterOption[];\n /** Placeholder text (for search type) */\n readonly placeholder?: string;\n}\n\nexport interface FilterOption {\n readonly value: string;\n readonly label: string;\n readonly disabled?: boolean;\n}\n\nexport interface FilterValues {\n [key: string]: string;\n}\n\nconst NO_OPTIONS: SelectOption[] = [];\n\n/**\n * Filter bar component for composable data filtering.\n *\n * Features:\n * - Select dropdowns, toggle groups, and search inputs\n * - Declarative filter configuration via FilterConfig array\n * - Two-way filter values binding\n * - Responsive horizontal flex layout\n * - Size variants (sm, md)\n * - Dark mode support\n *\n * @example\n * ```html\n * <lc-filter-bar\n * [filters]=\"filterConfig\"\n * [values]=\"currentFilters\"\n * (valuesChange)=\"onFilterChange($event)\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-filter-bar',\n standalone: true,\n imports: [FormsModule, SelectComponent, SearchInputComponent],\n templateUrl: './filter-bar.component.html',\n styleUrl: './filter-bar.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FilterBarComponent {\n private static nextId = 0;\n\n /** Per-instance id prefix for label/control linking */\n readonly barId = `lc-filter-bar-${++FilterBarComponent.nextId}`;\n\n /** Filter configurations */\n filters = input.required<readonly FilterConfig[]>();\n\n /** Current filter values (two-way bindable) */\n values = input<FilterValues>({});\n\n /** Size variant */\n size = input<'sm' | 'md'>('md');\n\n /** Placeholder of select filters without their own `placeholder`. */\n selectPlaceholder = input('Select an option');\n\n /** Placeholder of search filters without their own `placeholder`. */\n searchPlaceholder = input('Search...');\n\n /** Emitted when any filter value changes */\n valuesChange = output<FilterValues>();\n\n /** Computed size class */\n sizeClass = computed(() => `lc-filter-bar--${this.size()}`);\n\n /**\n * Select options per filter key, built once per `filters` change so the\n * lc-select `options` input keeps a stable reference across change detection.\n */\n private readonly selectOptionsByKey = computed(() => {\n const map = new Map<string, SelectOption[]>();\n for (const filter of this.filters()) {\n if (filter.type === 'select') {\n map.set(filter.key, (filter.options ?? []).map((opt) => ({ ...opt })));\n }\n }\n return map;\n });\n\n /** Get the current value for a filter key */\n getValue(key: string): string {\n return this.values()[key] ?? '';\n }\n\n /** Memoised select options for a filter key */\n selectOptions(key: string): SelectOption[] {\n return this.selectOptionsByKey().get(key) ?? NO_OPTIONS;\n }\n\n /** id of the control rendered for a filter (used by `<label for>`) */\n fieldId(key: string): string {\n return `${this.barId}-${key}`;\n }\n\n /** id of the label rendered for a filter (used by `aria-labelledby`) */\n labelId(key: string): string {\n return `${this.barId}-${key}-label`;\n }\n\n /** Handle select / toggle change */\n onFilterChange(key: string, value: string): void {\n const updated = { ...this.values(), [key]: value };\n this.valuesChange.emit(updated);\n }\n\n /** Handle search input */\n onSearchInput(key: string, value: string): void {\n this.onFilterChange(key, value);\n }\n\n normalizeSelectValue(value: string | number | null): string {\n return value == null ? '' : String(value);\n }\n\n /** Check if a toggle option is active */\n isToggleActive(key: string, optionValue: string): boolean {\n return this.getValue(key) === optionValue;\n }\n\n /** Get toggle button classes */\n getToggleClasses(key: string, option: FilterOption): string {\n const classes = ['lc-filter-bar__toggle-btn'];\n if (this.isToggleActive(key, option.value)) {\n classes.push('lc-filter-bar__toggle-btn--active');\n }\n if (option.disabled) {\n classes.push('lc-filter-bar__toggle-btn--disabled');\n }\n return classes.join(' ');\n }\n\n}\n","<div class=\"lc-filter-bar\" [class]=\"sizeClass()\">\n @for (filter of filters(); track filter.key) {\n <div class=\"lc-filter-bar__item\">\n @if (filter.label) {\n @if (filter.type === 'search') {\n <label class=\"lc-filter-bar__label\" [id]=\"labelId(filter.key)\" [for]=\"fieldId(filter.key)\">\n {{ filter.label }}\n </label>\n } @else {\n <span class=\"lc-filter-bar__label\" [id]=\"labelId(filter.key)\">{{ filter.label }}</span>\n }\n }\n\n @switch (filter.type) {\n @case ('select') {\n <lc-select\n class=\"lc-filter-bar__select\"\n [size]=\"size() === 'sm' ? 'sm' : 'md'\"\n [placeholder]=\"filter.placeholder ?? selectPlaceholder()\"\n [options]=\"selectOptions(filter.key)\"\n [ariaLabel]=\"filter.label || filter.placeholder || filter.key\"\n [ngModel]=\"getValue(filter.key)\"\n (ngModelChange)=\"onFilterChange(filter.key, normalizeSelectValue($event))\"\n />\n }\n\n @case ('toggle') {\n <div\n class=\"lc-filter-bar__toggle-group\"\n role=\"group\"\n [attr.aria-labelledby]=\"filter.label ? labelId(filter.key) : null\"\n [attr.aria-label]=\"filter.label ? null : filter.key\"\n >\n @for (opt of filter.options; track opt.value) {\n <button\n type=\"button\"\n [class]=\"getToggleClasses(filter.key, opt)\"\n [disabled]=\"opt.disabled ?? false\"\n [attr.aria-pressed]=\"isToggleActive(filter.key, opt.value)\"\n (click)=\"onFilterChange(filter.key, opt.value)\"\n >\n {{ opt.label }}\n </button>\n }\n </div>\n }\n\n @case ('search') {\n <lc-search-input\n class=\"lc-filter-bar__search\"\n [inputId]=\"fieldId(filter.key)\"\n [ariaLabelledBy]=\"filter.label ? labelId(filter.key) : undefined\"\n [ariaLabel]=\"filter.label ? undefined : filter.placeholder || filter.key\"\n [placeholder]=\"filter.placeholder ?? searchPlaceholder()\"\n [size]=\"size() === 'sm' ? 'sm' : 'md'\"\n [debounceMs]=\"0\"\n [value]=\"getValue(filter.key)\"\n (searchChange)=\"onSearchInput(filter.key, $event)\"\n />\n }\n }\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
@@ -347,7 +347,7 @@ var GalleryComponent = class GalleryComponent {
|
|
|
347
347
|
}],
|
|
348
348
|
ngImport: i0,
|
|
349
349
|
template: "<!-- Filters -->\n@if (showFilters()) {\n <div class=\"gallery__filters\">\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === null\"\n (click)=\"setFilter(null)\"\n >\n {{ filterAllLabel() }}\n </button>\n @for (cat of categories(); track cat) {\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === cat\"\n (click)=\"setFilter(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n}\n\n<!-- Grid / Masonry -->\n<div class=\"gallery__grid\" [class.gallery__grid--masonry]=\"layout() === 'masonry'\" [ngStyle]=\"gridStyle()\">\n @for (item of filteredItems(); track item.src; let i = $index) {\n <div\n class=\"gallery__item\"\n [class.gallery__item--loaded]=\"isLoaded(i)\"\n [class.gallery__item--clickable]=\"enableLightbox()\"\n [ngStyle]=\"itemStyle()\"\n (click)=\"openLightbox(i, $event)\"\n (keydown.enter)=\"openLightbox(i, $event)\"\n (keydown.space)=\"$event.preventDefault()\"\n (keyup.space)=\"openLightbox(i, $event)\"\n [attr.tabindex]=\"enableLightbox() ? 0 : null\"\n [attr.role]=\"enableLightbox() ? 'button' : null\"\n [attr.aria-label]=\"itemAriaLabel(item, i)\"\n >\n @if (!isLoaded(i)) {\n <div class=\"gallery__placeholder\"></div>\n }\n <img\n class=\"gallery__thumb\"\n [src]=\"getThumbnail(item)\"\n [alt]=\"item.alt || ''\"\n loading=\"lazy\"\n (load)=\"onImageLoad(i)\"\n />\n @if (showCaptions() && item.caption) {\n <div class=\"gallery__caption\">{{ item.caption }}</div>\n }\n </div>\n }\n</div>\n\n<!-- Lightbox overlay -->\n@if (lightboxItem(); as item) {\n <div\n class=\"gallery__lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"lightboxAriaLabel()\"\n tabindex=\"-1\"\n cdkTrapFocus\n (keydown)=\"onKeydown($event)\"\n >\n <!-- Backdrop -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the lightbox -->\n <div class=\"gallery__lightbox-backdrop\" (click)=\"closeLightbox()\"></div>\n\n <!-- Toolbar -->\n <div class=\"gallery__lightbox-toolbar\">\n <div class=\"gallery__lightbox-counter\">{{ lightboxCounter() }}</div>\n <div class=\"gallery__lightbox-actions\">\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomOutAriaLabel()\" (click)=\"zoomOut()\">\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <span class=\"gallery__lightbox-zoom\">{{ lightboxZoom() }}%</span>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomInAriaLabel()\" (click)=\"zoomIn()\">\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"resetZoomAriaLabel()\" (click)=\"resetZoom()\">\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"downloadAriaLabel()\" (click)=\"downloadCurrent()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button #closeBtn type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"closeAriaLabel()\" (click)=\"closeLightbox()\">\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n </div>\n\n <!-- Navigation -->\n @if (canGoPrev()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--prev\"\n [attr.aria-label]=\"previousAriaLabel()\"\n (click)=\"goToPrev()\"\n >\n <lc-icon name=\"chevron-left\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n @if (canGoNext()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--next\"\n [attr.aria-label]=\"nextAriaLabel()\"\n (click)=\"goToNext()\"\n >\n <lc-icon name=\"chevron-right\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Image -->\n <div class=\"gallery__lightbox-content\">\n <img\n class=\"gallery__lightbox-image\"\n [src]=\"item.src\"\n [alt]=\"item.alt || ''\"\n [style.transform]=\"lightboxTransform()\"\n />\n </div>\n\n <!-- Caption -->\n @if (item.caption) {\n <div class=\"gallery__lightbox-caption\">{{ item.caption }}</div>\n }\n </div>\n}\n",
|
|
350
|
-
styles: [".gallery{width:100%}.gallery__filters{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem}.gallery__filter-btn{padding:.375rem .75rem;border:1px solid var(--color-neutral-200);border-radius:999px;background:transparent;color:var(--color-text-secondary);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);cursor:pointer;transition:all .15s ease}.gallery__filter-btn:hover{border-color:var(--color-primary-300);color:var(--color-primary-500)}.gallery__filter-btn--active{background:var(--color-primary-500);border-color:var(--color-primary-500);color:#fff}.gallery__filter-btn--active:hover{background:var(--color-primary-600);border-color:var(--color-primary-600);color:#fff}.gallery__grid{width:100%}.gallery__grid--masonry{display:block!important}.gallery__item{position:relative;overflow:hidden;border-radius:.5rem;background:var(--color-neutral-100)}.gallery__item--clickable{cursor:pointer}.gallery__item--clickable:hover .gallery__thumb{transform:scale(1.05)}.gallery__item--clickable:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.gallery__grid--masonry .gallery__item{margin-bottom:8px;break-inside:avoid}.gallery__placeholder{position:absolute;inset:0;background:linear-gradient(90deg,var(--color-neutral-100) 25%,var(--color-neutral-200) 50%,var(--color-neutral-100) 75%);background-size:200% 100%;animation:gallery-shimmer 1.5s ease-in-out infinite}.gallery__thumb{display:block;width:100%;height:100%;object-fit:cover;transition:transform .3s ease;opacity:0}.gallery__item--loaded .gallery__thumb{opacity:1}.gallery__caption{position:absolute;bottom:0;left:0;right:0;padding:.5rem .75rem;background:linear-gradient(transparent,#000000b3);color:#fff;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.gallery__lightbox{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center}.gallery__lightbox:focus{outline:none}.gallery__lightbox-backdrop{position:absolute;inset:0;background:#000000e6}.gallery__lightbox-toolbar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;z-index:2}.gallery__lightbox-counter{color:#ffffffb3;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);-webkit-user-select:none;user-select:none}.gallery__lightbox-actions{display:flex;align-items:center;gap:.25rem}.gallery__lightbox-zoom{min-width:3rem;text-align:center;color:#ffffffb3;font-size:var(--typography-font-size-xs, .75rem);-webkit-user-select:none;user-select:none}.gallery__lightbox-btn{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:none;border-radius:.375rem;background:transparent;color:#ffffffb3;cursor:pointer;transition:background-color .15s ease,color .15s ease}.gallery__lightbox-btn:hover{background:#ffffff26;color:#fff}.gallery__lightbox-btn:focus-visible{outline:2px solid #fff;outline-offset:2px;color:#fff}.gallery__lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:flex;align-items:center;justify-content:center;width:3rem;height:3rem;border:none;border-radius:50%;background:#00000080;color:#fff;cursor:pointer;transition:background-color .15s ease}.gallery__lightbox-nav:hover{background:#000c}.gallery__lightbox-nav:focus-visible{outline:2px solid #fff;outline-offset:2px}.gallery__lightbox-nav--prev{left:1rem}.gallery__lightbox-nav--next{right:1rem}.gallery__lightbox-content{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;max-width:90vw;max-height:80vh;overflow:auto}.gallery__lightbox-image{max-width:90vw;max-height:80vh;object-fit:contain;transition:transform .2s ease;transform-origin:center center;-webkit-user-select:none;user-select:none}.gallery__lightbox-caption{position:absolute;bottom:0;left:0;right:0;text-align:center;padding:1rem;color:#ffffffe6;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);z-index:2}@keyframes gallery-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"],
|
|
350
|
+
styles: [".gallery{width:100%}.gallery__filters{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem}.gallery__filter-btn{padding:.375rem .75rem;border:1px solid var(--color-neutral-200);border-radius:999px;background:transparent;color:var(--color-text-secondary);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);cursor:pointer;transition:all .15s ease}.gallery__filter-btn:hover{border-color:var(--color-primary-300);color:var(--color-primary-500)}.gallery__filter-btn--active{background:var(--color-primary-500);border-color:var(--color-primary-500);color:#fff}.gallery__filter-btn--active:hover{background:var(--color-primary-600);border-color:var(--color-primary-600);color:#fff}.gallery__filter-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.gallery__grid{width:100%}.gallery__grid--masonry{display:block!important}.gallery__item{position:relative;overflow:hidden;border-radius:.5rem;background:var(--color-neutral-100)}.gallery__item--clickable{cursor:pointer}.gallery__item--clickable:hover .gallery__thumb{transform:scale(1.05)}.gallery__item--clickable:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.gallery__grid--masonry .gallery__item{margin-bottom:8px;break-inside:avoid}.gallery__placeholder{position:absolute;inset:0;background:linear-gradient(90deg,var(--color-neutral-100) 25%,var(--color-neutral-200) 50%,var(--color-neutral-100) 75%);background-size:200% 100%;animation:gallery-shimmer 1.5s ease-in-out infinite}.gallery__thumb{display:block;width:100%;height:100%;object-fit:cover;transition:transform .3s ease;opacity:0}.gallery__item--loaded .gallery__thumb{opacity:1}.gallery__caption{position:absolute;bottom:0;left:0;right:0;padding:.5rem .75rem;background:linear-gradient(transparent,#000000b3);color:#fff;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.gallery__lightbox{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center}.gallery__lightbox:focus{outline:none}.gallery__lightbox-backdrop{position:absolute;inset:0;background:#000000e6}.gallery__lightbox-toolbar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;z-index:2}.gallery__lightbox-counter{color:#ffffffb3;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);-webkit-user-select:none;user-select:none}.gallery__lightbox-actions{display:flex;align-items:center;gap:.25rem}.gallery__lightbox-zoom{min-width:3rem;text-align:center;color:#ffffffb3;font-size:var(--typography-font-size-xs, .75rem);-webkit-user-select:none;user-select:none}.gallery__lightbox-btn{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:none;border-radius:.375rem;background:transparent;color:#ffffffb3;cursor:pointer;transition:background-color .15s ease,color .15s ease}.gallery__lightbox-btn:hover{background:#ffffff26;color:#fff}.gallery__lightbox-btn:focus-visible{outline:2px solid #fff;outline-offset:2px;color:#fff}.gallery__lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:flex;align-items:center;justify-content:center;width:3rem;height:3rem;border:none;border-radius:50%;background:#00000080;color:#fff;cursor:pointer;transition:background-color .15s ease}.gallery__lightbox-nav:hover{background:#000c}.gallery__lightbox-nav:focus-visible{outline:2px solid #fff;outline-offset:2px}.gallery__lightbox-nav--prev{left:1rem}.gallery__lightbox-nav--next{right:1rem}.gallery__lightbox-content{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;max-width:90vw;max-height:80vh;overflow:auto}.gallery__lightbox-image{max-width:90vw;max-height:80vh;object-fit:contain;transition:transform .2s ease;transform-origin:center center;-webkit-user-select:none;user-select:none}.gallery__lightbox-caption{position:absolute;bottom:0;left:0;right:0;text-align:center;padding:1rem;color:#ffffffe6;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);z-index:2}@keyframes gallery-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media(forced-colors:active){.gallery__filter-btn:focus-visible{outline-color:Highlight}}\n"],
|
|
351
351
|
dependencies: [
|
|
352
352
|
{
|
|
353
353
|
kind: "component",
|
|
@@ -410,7 +410,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
410
410
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
411
411
|
host: { "[class]": "\"gallery\"" },
|
|
412
412
|
template: "<!-- Filters -->\n@if (showFilters()) {\n <div class=\"gallery__filters\">\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === null\"\n (click)=\"setFilter(null)\"\n >\n {{ filterAllLabel() }}\n </button>\n @for (cat of categories(); track cat) {\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === cat\"\n (click)=\"setFilter(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n}\n\n<!-- Grid / Masonry -->\n<div class=\"gallery__grid\" [class.gallery__grid--masonry]=\"layout() === 'masonry'\" [ngStyle]=\"gridStyle()\">\n @for (item of filteredItems(); track item.src; let i = $index) {\n <div\n class=\"gallery__item\"\n [class.gallery__item--loaded]=\"isLoaded(i)\"\n [class.gallery__item--clickable]=\"enableLightbox()\"\n [ngStyle]=\"itemStyle()\"\n (click)=\"openLightbox(i, $event)\"\n (keydown.enter)=\"openLightbox(i, $event)\"\n (keydown.space)=\"$event.preventDefault()\"\n (keyup.space)=\"openLightbox(i, $event)\"\n [attr.tabindex]=\"enableLightbox() ? 0 : null\"\n [attr.role]=\"enableLightbox() ? 'button' : null\"\n [attr.aria-label]=\"itemAriaLabel(item, i)\"\n >\n @if (!isLoaded(i)) {\n <div class=\"gallery__placeholder\"></div>\n }\n <img\n class=\"gallery__thumb\"\n [src]=\"getThumbnail(item)\"\n [alt]=\"item.alt || ''\"\n loading=\"lazy\"\n (load)=\"onImageLoad(i)\"\n />\n @if (showCaptions() && item.caption) {\n <div class=\"gallery__caption\">{{ item.caption }}</div>\n }\n </div>\n }\n</div>\n\n<!-- Lightbox overlay -->\n@if (lightboxItem(); as item) {\n <div\n class=\"gallery__lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"lightboxAriaLabel()\"\n tabindex=\"-1\"\n cdkTrapFocus\n (keydown)=\"onKeydown($event)\"\n >\n <!-- Backdrop -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the lightbox -->\n <div class=\"gallery__lightbox-backdrop\" (click)=\"closeLightbox()\"></div>\n\n <!-- Toolbar -->\n <div class=\"gallery__lightbox-toolbar\">\n <div class=\"gallery__lightbox-counter\">{{ lightboxCounter() }}</div>\n <div class=\"gallery__lightbox-actions\">\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomOutAriaLabel()\" (click)=\"zoomOut()\">\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <span class=\"gallery__lightbox-zoom\">{{ lightboxZoom() }}%</span>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomInAriaLabel()\" (click)=\"zoomIn()\">\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"resetZoomAriaLabel()\" (click)=\"resetZoom()\">\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"downloadAriaLabel()\" (click)=\"downloadCurrent()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button #closeBtn type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"closeAriaLabel()\" (click)=\"closeLightbox()\">\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n </div>\n\n <!-- Navigation -->\n @if (canGoPrev()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--prev\"\n [attr.aria-label]=\"previousAriaLabel()\"\n (click)=\"goToPrev()\"\n >\n <lc-icon name=\"chevron-left\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n @if (canGoNext()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--next\"\n [attr.aria-label]=\"nextAriaLabel()\"\n (click)=\"goToNext()\"\n >\n <lc-icon name=\"chevron-right\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Image -->\n <div class=\"gallery__lightbox-content\">\n <img\n class=\"gallery__lightbox-image\"\n [src]=\"item.src\"\n [alt]=\"item.alt || ''\"\n [style.transform]=\"lightboxTransform()\"\n />\n </div>\n\n <!-- Caption -->\n @if (item.caption) {\n <div class=\"gallery__lightbox-caption\">{{ item.caption }}</div>\n }\n </div>\n}\n",
|
|
413
|
-
styles: [".gallery{width:100%}.gallery__filters{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem}.gallery__filter-btn{padding:.375rem .75rem;border:1px solid var(--color-neutral-200);border-radius:999px;background:transparent;color:var(--color-text-secondary);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);cursor:pointer;transition:all .15s ease}.gallery__filter-btn:hover{border-color:var(--color-primary-300);color:var(--color-primary-500)}.gallery__filter-btn--active{background:var(--color-primary-500);border-color:var(--color-primary-500);color:#fff}.gallery__filter-btn--active:hover{background:var(--color-primary-600);border-color:var(--color-primary-600);color:#fff}.gallery__grid{width:100%}.gallery__grid--masonry{display:block!important}.gallery__item{position:relative;overflow:hidden;border-radius:.5rem;background:var(--color-neutral-100)}.gallery__item--clickable{cursor:pointer}.gallery__item--clickable:hover .gallery__thumb{transform:scale(1.05)}.gallery__item--clickable:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.gallery__grid--masonry .gallery__item{margin-bottom:8px;break-inside:avoid}.gallery__placeholder{position:absolute;inset:0;background:linear-gradient(90deg,var(--color-neutral-100) 25%,var(--color-neutral-200) 50%,var(--color-neutral-100) 75%);background-size:200% 100%;animation:gallery-shimmer 1.5s ease-in-out infinite}.gallery__thumb{display:block;width:100%;height:100%;object-fit:cover;transition:transform .3s ease;opacity:0}.gallery__item--loaded .gallery__thumb{opacity:1}.gallery__caption{position:absolute;bottom:0;left:0;right:0;padding:.5rem .75rem;background:linear-gradient(transparent,#000000b3);color:#fff;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.gallery__lightbox{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center}.gallery__lightbox:focus{outline:none}.gallery__lightbox-backdrop{position:absolute;inset:0;background:#000000e6}.gallery__lightbox-toolbar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;z-index:2}.gallery__lightbox-counter{color:#ffffffb3;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);-webkit-user-select:none;user-select:none}.gallery__lightbox-actions{display:flex;align-items:center;gap:.25rem}.gallery__lightbox-zoom{min-width:3rem;text-align:center;color:#ffffffb3;font-size:var(--typography-font-size-xs, .75rem);-webkit-user-select:none;user-select:none}.gallery__lightbox-btn{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:none;border-radius:.375rem;background:transparent;color:#ffffffb3;cursor:pointer;transition:background-color .15s ease,color .15s ease}.gallery__lightbox-btn:hover{background:#ffffff26;color:#fff}.gallery__lightbox-btn:focus-visible{outline:2px solid #fff;outline-offset:2px;color:#fff}.gallery__lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:flex;align-items:center;justify-content:center;width:3rem;height:3rem;border:none;border-radius:50%;background:#00000080;color:#fff;cursor:pointer;transition:background-color .15s ease}.gallery__lightbox-nav:hover{background:#000c}.gallery__lightbox-nav:focus-visible{outline:2px solid #fff;outline-offset:2px}.gallery__lightbox-nav--prev{left:1rem}.gallery__lightbox-nav--next{right:1rem}.gallery__lightbox-content{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;max-width:90vw;max-height:80vh;overflow:auto}.gallery__lightbox-image{max-width:90vw;max-height:80vh;object-fit:contain;transition:transform .2s ease;transform-origin:center center;-webkit-user-select:none;user-select:none}.gallery__lightbox-caption{position:absolute;bottom:0;left:0;right:0;text-align:center;padding:1rem;color:#ffffffe6;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);z-index:2}@keyframes gallery-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"]
|
|
413
|
+
styles: [".gallery{width:100%}.gallery__filters{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem}.gallery__filter-btn{padding:.375rem .75rem;border:1px solid var(--color-neutral-200);border-radius:999px;background:transparent;color:var(--color-text-secondary);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);cursor:pointer;transition:all .15s ease}.gallery__filter-btn:hover{border-color:var(--color-primary-300);color:var(--color-primary-500)}.gallery__filter-btn--active{background:var(--color-primary-500);border-color:var(--color-primary-500);color:#fff}.gallery__filter-btn--active:hover{background:var(--color-primary-600);border-color:var(--color-primary-600);color:#fff}.gallery__filter-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.gallery__grid{width:100%}.gallery__grid--masonry{display:block!important}.gallery__item{position:relative;overflow:hidden;border-radius:.5rem;background:var(--color-neutral-100)}.gallery__item--clickable{cursor:pointer}.gallery__item--clickable:hover .gallery__thumb{transform:scale(1.05)}.gallery__item--clickable:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.gallery__grid--masonry .gallery__item{margin-bottom:8px;break-inside:avoid}.gallery__placeholder{position:absolute;inset:0;background:linear-gradient(90deg,var(--color-neutral-100) 25%,var(--color-neutral-200) 50%,var(--color-neutral-100) 75%);background-size:200% 100%;animation:gallery-shimmer 1.5s ease-in-out infinite}.gallery__thumb{display:block;width:100%;height:100%;object-fit:cover;transition:transform .3s ease;opacity:0}.gallery__item--loaded .gallery__thumb{opacity:1}.gallery__caption{position:absolute;bottom:0;left:0;right:0;padding:.5rem .75rem;background:linear-gradient(transparent,#000000b3);color:#fff;font-size:var(--typography-font-size-xs, .75rem);line-height:1.4}.gallery__lightbox{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center}.gallery__lightbox:focus{outline:none}.gallery__lightbox-backdrop{position:absolute;inset:0;background:#000000e6}.gallery__lightbox-toolbar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;z-index:2}.gallery__lightbox-counter{color:#ffffffb3;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);-webkit-user-select:none;user-select:none}.gallery__lightbox-actions{display:flex;align-items:center;gap:.25rem}.gallery__lightbox-zoom{min-width:3rem;text-align:center;color:#ffffffb3;font-size:var(--typography-font-size-xs, .75rem);-webkit-user-select:none;user-select:none}.gallery__lightbox-btn{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:none;border-radius:.375rem;background:transparent;color:#ffffffb3;cursor:pointer;transition:background-color .15s ease,color .15s ease}.gallery__lightbox-btn:hover{background:#ffffff26;color:#fff}.gallery__lightbox-btn:focus-visible{outline:2px solid #fff;outline-offset:2px;color:#fff}.gallery__lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;display:flex;align-items:center;justify-content:center;width:3rem;height:3rem;border:none;border-radius:50%;background:#00000080;color:#fff;cursor:pointer;transition:background-color .15s ease}.gallery__lightbox-nav:hover{background:#000c}.gallery__lightbox-nav:focus-visible{outline:2px solid #fff;outline-offset:2px}.gallery__lightbox-nav--prev{left:1rem}.gallery__lightbox-nav--next{right:1rem}.gallery__lightbox-content{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;max-width:90vw;max-height:80vh;overflow:auto}.gallery__lightbox-image{max-width:90vw;max-height:80vh;object-fit:contain;transition:transform .2s ease;transform-origin:center center;-webkit-user-select:none;user-select:none}.gallery__lightbox-caption{position:absolute;bottom:0;left:0;right:0;text-align:center;padding:1rem;color:#ffffffe6;font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family, sans-serif);z-index:2}@keyframes gallery-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media(forced-colors:active){.gallery__filter-btn:focus-visible{outline-color:Highlight}}\n"]
|
|
414
414
|
}]
|
|
415
415
|
}],
|
|
416
416
|
propDecorators: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-gallery.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.ts","../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n HostListener,\n Injector,\n OnDestroy,\n afterNextRender,\n computed,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { CdkTrapFocus } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT, ICON_PLUS, ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { NgStyle } from '@angular/common';\nimport { isSafeImageUrl } from '@lifecorner/angular-ui-kit/internal/url';\nimport { nameWithText } from '@lifecorner/angular-ui-kit/internal/a11y';\n\nexport type GalleryLayout = 'grid' | 'masonry';\nexport type GallerySize = 'sm' | 'md' | 'lg';\n\nexport interface GalleryItem {\n /** Image source URL */\n src: string;\n /** Thumbnail URL (falls back to src if not provided) */\n thumbnail?: string;\n /** Alt text for accessibility */\n alt?: string;\n /** Optional caption displayed below the image */\n caption?: string;\n /** Optional category for filtering */\n category?: string;\n}\n\nconst SIZE_COLUMNS: Record<GallerySize, number> = {\n sm: 4,\n md: 3,\n lg: 2,\n};\n\n/**\n * Gallery component for displaying image collections with lightbox.\n *\n * Features:\n * - Grid and masonry layout modes\n * - Responsive column count via size presets (sm, md, lg thumbnails)\n * - Configurable custom column count\n * - Lightbox overlay with navigation (prev/next)\n * - Keyboard navigation (Arrow keys, Escape); focus is trapped inside the\n * lightbox and returned to the originating thumbnail on close\n * - Optional captions and category filtering\n * - Lazy loading with placeholder shimmer\n * - Zoom control in lightbox\n * - Download button in lightbox\n * - Dark/light theme support\n *\n * @example\n * ```html\n * <lc-gallery\n * [items]=\"images\"\n * layout=\"grid\"\n * size=\"md\"\n * [showCaptions]=\"true\" />\n * ```\n */\n@Component({\n selector: 'lc-gallery',\n standalone: true,\n imports: [IconComponent, NgStyle, CdkTrapFocus],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS, ICON_X_MARK, ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT])],\n templateUrl: './gallery.component.html',\n styleUrls: ['./gallery.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': '\"gallery\"',\n },\n})\nexport class GalleryComponent implements OnDestroy {\n /** Array of gallery items to display */\n readonly items = input.required<GalleryItem[]>();\n\n /** Layout mode */\n readonly layout = input<GalleryLayout>('grid');\n\n /** Thumbnail size preset — controls column count (sm=4col, md=3col, lg=2col) */\n readonly size = input<GallerySize>('md');\n\n /** Custom column count (overrides size preset) */\n readonly columns = input<number | null>(null);\n\n /**\n * Whether to show captions below thumbnails. A shown caption is part of the\n * item's accessible name: `alt` (or `imageFallbackLabel` and its number),\n * then the caption (WCAG 2.5.3 Label in Name).\n * @default false\n */\n readonly showCaptions = input(false);\n\n /** Whether clicking a thumbnail opens the lightbox */\n readonly enableLightbox = input(true);\n\n /** Gap between items in pixels */\n readonly gap = input(8);\n\n /** Aspect ratio for grid items (ignored in masonry mode) */\n readonly aspectRatio = input('1 / 1');\n\n /** Label of the \"all\" category filter (shown when the items span more than one category). */\n readonly filterAllLabel = input('All');\n\n /** Accessible name of the lightbox dialog. */\n readonly lightboxAriaLabel = input('Image lightbox');\n\n /** Accessible name of the lightbox zoom-out button. */\n readonly zoomOutAriaLabel = input('Zoom out');\n\n /** Accessible name of the lightbox zoom-in button. */\n readonly zoomInAriaLabel = input('Zoom in');\n\n /** Accessible name of the lightbox reset-zoom button. */\n readonly resetZoomAriaLabel = input('Reset zoom');\n\n /** Accessible name of the lightbox download button. */\n readonly downloadAriaLabel = input('Download');\n\n /** Accessible name of the lightbox close button. */\n readonly closeAriaLabel = input('Close');\n\n /** Accessible name of the previous-image button. */\n readonly previousAriaLabel = input('Previous image');\n\n /** Accessible name of the next-image button. */\n readonly nextAriaLabel = input('Next image');\n\n /**\n * Accessible name of a thumbnail without `alt`, followed by its number. A\n * shown caption follows (`Bild 3, Sonnenuntergang am See`).\n * @default 'Image'\n */\n readonly imageFallbackLabel = input('Image');\n\n /** Emitted when lightbox opens with item index */\n readonly lightboxOpen = output<number>();\n\n /** Emitted when lightbox closes */\n readonly lightboxClose = output<void>();\n\n // ── Internal state ───────────────────────────────────────────────────\n\n private readonly injector = inject(Injector);\n private readonly closeBtn = viewChild<ElementRef<HTMLButtonElement>>('closeBtn');\n /** Element that opened the lightbox — focus returns there on close. */\n private triggerElement: HTMLElement | null = null;\n\n protected lightboxIndex = signal<number | null>(null);\n protected lightboxZoom = signal(100);\n protected loadedImages = signal<Set<number>>(new Set());\n protected activeFilter = signal<string | null>(null);\n\n // ── Computed ─────────────────────────────────────────────────────────\n\n protected gridColumns = computed(() => {\n return this.columns() ?? SIZE_COLUMNS[this.size()];\n });\n\n protected gridStyle = computed(() => {\n const cols = this.gridColumns();\n const g = this.gap();\n if (this.layout() === 'masonry') {\n return {\n 'column-count': `${cols}`,\n 'column-gap': `${g}px`,\n };\n }\n return {\n 'display': 'grid',\n 'grid-template-columns': `repeat(${cols}, 1fr)`,\n 'gap': `${g}px`,\n };\n });\n\n protected itemStyle = computed(() => {\n if (this.layout() === 'masonry') return {};\n return { 'aspect-ratio': this.aspectRatio() };\n });\n\n protected categories = computed<string[]>(() => {\n const cats = new Set<string>();\n for (const item of this.items()) {\n if (item.category) cats.add(item.category);\n }\n return Array.from(cats).sort();\n });\n\n protected filteredItems = computed(() => {\n const filter = this.activeFilter();\n if (!filter) return this.items();\n return this.items().filter(item => item.category === filter);\n });\n\n protected showFilters = computed(() => this.categories().length > 1);\n\n protected lightboxItem = computed<GalleryItem | null>(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return null;\n const items = this.filteredItems();\n return items[idx] ?? null;\n });\n\n protected lightboxCounter = computed(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return '';\n return `${idx + 1} / ${this.filteredItems().length}`;\n });\n\n protected lightboxTransform = computed(() => {\n const z = this.lightboxZoom() / 100;\n return `scale(${z})`;\n });\n\n protected canGoPrev = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx > 0;\n });\n\n protected canGoNext = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx < this.filteredItems().length - 1;\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────\n\n ngOnDestroy(): void {\n // No focus restore here: the injector is going away with the view.\n this.closeLightbox(false);\n }\n\n // ── Actions ──────────────────────────────────────────────────────────\n\n /**\n * Opens the lightbox for `index`. Like a native button, Space activates on\n * *keyup* (keydown only prevents the page scroll): activating on keydown\n * would move focus to the Close button before the key is released, and the\n * Space keyup would then immediately click Close.\n */\n protected openLightbox(index: number, event?: Event): void {\n if (!this.enableLightbox()) return;\n if (event instanceof KeyboardEvent) event.preventDefault();\n this.triggerElement =\n (event?.currentTarget as HTMLElement | null) ??\n (document.activeElement instanceof HTMLElement ? document.activeElement : null);\n this.lightboxIndex.set(index);\n this.lightboxZoom.set(100);\n this.lightboxOpen.emit(index);\n document.body.style.overflow = 'hidden';\n // The dialog is `@if`-rendered — move focus in once it exists.\n afterNextRender(() => this.closeBtn()?.nativeElement.focus(), { injector: this.injector });\n }\n\n protected closeLightbox(restoreFocus = true): void {\n if (this.lightboxIndex() === null) return;\n this.lightboxIndex.set(null);\n this.lightboxZoom.set(100);\n this.lightboxClose.emit();\n document.body.style.overflow = '';\n const trigger = this.triggerElement;\n this.triggerElement = null;\n if (restoreFocus && trigger?.isConnected) {\n afterNextRender(() => trigger.focus(), { injector: this.injector });\n }\n }\n\n protected goToPrev(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx > 0) {\n this.lightboxIndex.set(idx - 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected goToNext(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx < this.filteredItems().length - 1) {\n this.lightboxIndex.set(idx + 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected zoomIn(): void {\n this.lightboxZoom.update(z => Math.min(z + 25, 500));\n }\n\n protected zoomOut(): void {\n this.lightboxZoom.update(z => Math.max(z - 25, 25));\n }\n\n protected resetZoom(): void {\n this.lightboxZoom.set(100);\n }\n\n protected downloadCurrent(): void {\n const item = this.lightboxItem();\n // A programmatic click on a caller-supplied URL: only image-safe schemes.\n if (!item || !isSafeImageUrl(item.src)) return;\n const a = document.createElement('a');\n a.href = item.src;\n a.download = item.alt || 'image';\n a.target = '_blank';\n a.rel = 'noopener noreferrer';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }\n\n protected setFilter(category: string | null): void {\n this.activeFilter.set(category);\n }\n\n protected onImageLoad(index: number): void {\n this.loadedImages.update(set => {\n const next = new Set(set);\n next.add(index);\n return next;\n });\n }\n\n protected isLoaded(index: number): boolean {\n return this.loadedImages().has(index);\n }\n\n protected getThumbnail(item: GalleryItem): string {\n return item.thumbnail || item.src;\n }\n\n /**\n * Name of a thumbnail (AS-33, WCAG 2.5.3 Label in Name): `alt` (or\n * `imageFallbackLabel` and its number), then the shown caption unless one\n * contains the other.\n */\n protected itemAriaLabel(item: GalleryItem, index: number): string {\n const label = item.alt || `${this.imageFallbackLabel()} ${index + 1}`;\n return nameWithText(label, this.showCaptions() ? (item.caption ?? '') : '');\n }\n\n protected trackByIndex(index: number): number {\n return index;\n }\n\n /**\n * Keyboard handling while the lightbox is open. Bound on the lightbox itself\n * (focus is trapped inside it) and, as a fallback, on `document`. A handled\n * Escape stops propagating so an enclosing modal/drawer doesn't close too.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (this.lightboxIndex() === null) return;\n switch (event.key) {\n case 'Escape':\n event.stopPropagation();\n this.closeLightbox();\n break;\n case 'ArrowLeft':\n event.stopPropagation();\n this.goToPrev();\n break;\n case 'ArrowRight':\n event.stopPropagation();\n this.goToNext();\n break;\n }\n }\n}\n","<!-- Filters -->\n@if (showFilters()) {\n <div class=\"gallery__filters\">\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === null\"\n (click)=\"setFilter(null)\"\n >\n {{ filterAllLabel() }}\n </button>\n @for (cat of categories(); track cat) {\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === cat\"\n (click)=\"setFilter(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n}\n\n<!-- Grid / Masonry -->\n<div class=\"gallery__grid\" [class.gallery__grid--masonry]=\"layout() === 'masonry'\" [ngStyle]=\"gridStyle()\">\n @for (item of filteredItems(); track item.src; let i = $index) {\n <div\n class=\"gallery__item\"\n [class.gallery__item--loaded]=\"isLoaded(i)\"\n [class.gallery__item--clickable]=\"enableLightbox()\"\n [ngStyle]=\"itemStyle()\"\n (click)=\"openLightbox(i, $event)\"\n (keydown.enter)=\"openLightbox(i, $event)\"\n (keydown.space)=\"$event.preventDefault()\"\n (keyup.space)=\"openLightbox(i, $event)\"\n [attr.tabindex]=\"enableLightbox() ? 0 : null\"\n [attr.role]=\"enableLightbox() ? 'button' : null\"\n [attr.aria-label]=\"itemAriaLabel(item, i)\"\n >\n @if (!isLoaded(i)) {\n <div class=\"gallery__placeholder\"></div>\n }\n <img\n class=\"gallery__thumb\"\n [src]=\"getThumbnail(item)\"\n [alt]=\"item.alt || ''\"\n loading=\"lazy\"\n (load)=\"onImageLoad(i)\"\n />\n @if (showCaptions() && item.caption) {\n <div class=\"gallery__caption\">{{ item.caption }}</div>\n }\n </div>\n }\n</div>\n\n<!-- Lightbox overlay -->\n@if (lightboxItem(); as item) {\n <div\n class=\"gallery__lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"lightboxAriaLabel()\"\n tabindex=\"-1\"\n cdkTrapFocus\n (keydown)=\"onKeydown($event)\"\n >\n <!-- Backdrop -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the lightbox -->\n <div class=\"gallery__lightbox-backdrop\" (click)=\"closeLightbox()\"></div>\n\n <!-- Toolbar -->\n <div class=\"gallery__lightbox-toolbar\">\n <div class=\"gallery__lightbox-counter\">{{ lightboxCounter() }}</div>\n <div class=\"gallery__lightbox-actions\">\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomOutAriaLabel()\" (click)=\"zoomOut()\">\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <span class=\"gallery__lightbox-zoom\">{{ lightboxZoom() }}%</span>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomInAriaLabel()\" (click)=\"zoomIn()\">\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"resetZoomAriaLabel()\" (click)=\"resetZoom()\">\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"downloadAriaLabel()\" (click)=\"downloadCurrent()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button #closeBtn type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"closeAriaLabel()\" (click)=\"closeLightbox()\">\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n </div>\n\n <!-- Navigation -->\n @if (canGoPrev()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--prev\"\n [attr.aria-label]=\"previousAriaLabel()\"\n (click)=\"goToPrev()\"\n >\n <lc-icon name=\"chevron-left\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n @if (canGoNext()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--next\"\n [attr.aria-label]=\"nextAriaLabel()\"\n (click)=\"goToNext()\"\n >\n <lc-icon name=\"chevron-right\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Image -->\n <div class=\"gallery__lightbox-content\">\n <img\n class=\"gallery__lightbox-image\"\n [src]=\"item.src\"\n [alt]=\"item.alt || ''\"\n [style.transform]=\"lightboxTransform()\"\n />\n </div>\n\n <!-- Caption -->\n @if (item.caption) {\n <div class=\"gallery__lightbox-caption\">{{ item.caption }}</div>\n }\n </div>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-gallery.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.ts","../../../../libs/angular-ui-kit/src/lib/gallery/gallery.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n HostListener,\n Injector,\n OnDestroy,\n afterNextRender,\n computed,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { CdkTrapFocus } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT, ICON_PLUS, ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { NgStyle } from '@angular/common';\nimport { isSafeImageUrl } from '@lifecorner/angular-ui-kit/internal/url';\nimport { nameWithText } from '@lifecorner/angular-ui-kit/internal/a11y';\n\nexport type GalleryLayout = 'grid' | 'masonry';\nexport type GallerySize = 'sm' | 'md' | 'lg';\n\nexport interface GalleryItem {\n /** Image source URL */\n src: string;\n /** Thumbnail URL (falls back to src if not provided) */\n thumbnail?: string;\n /** Alt text for accessibility */\n alt?: string;\n /** Optional caption displayed below the image */\n caption?: string;\n /** Optional category for filtering */\n category?: string;\n}\n\nconst SIZE_COLUMNS: Record<GallerySize, number> = {\n sm: 4,\n md: 3,\n lg: 2,\n};\n\n/**\n * Gallery component for displaying image collections with lightbox.\n *\n * Features:\n * - Grid and masonry layout modes\n * - Responsive column count via size presets (sm, md, lg thumbnails)\n * - Configurable custom column count\n * - Lightbox overlay with navigation (prev/next)\n * - Keyboard navigation (Arrow keys, Escape); focus is trapped inside the\n * lightbox and returned to the originating thumbnail on close\n * - Optional captions and category filtering\n * - Lazy loading with placeholder shimmer\n * - Zoom control in lightbox\n * - Download button in lightbox\n * - Dark/light theme support\n *\n * @example\n * ```html\n * <lc-gallery\n * [items]=\"images\"\n * layout=\"grid\"\n * size=\"md\"\n * [showCaptions]=\"true\" />\n * ```\n */\n@Component({\n selector: 'lc-gallery',\n standalone: true,\n imports: [IconComponent, NgStyle, CdkTrapFocus],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS, ICON_X_MARK, ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT])],\n templateUrl: './gallery.component.html',\n styleUrls: ['./gallery.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': '\"gallery\"',\n },\n})\nexport class GalleryComponent implements OnDestroy {\n /** Array of gallery items to display */\n readonly items = input.required<GalleryItem[]>();\n\n /** Layout mode */\n readonly layout = input<GalleryLayout>('grid');\n\n /** Thumbnail size preset — controls column count (sm=4col, md=3col, lg=2col) */\n readonly size = input<GallerySize>('md');\n\n /** Custom column count (overrides size preset) */\n readonly columns = input<number | null>(null);\n\n /**\n * Whether to show captions below thumbnails. A shown caption is part of the\n * item's accessible name: `alt` (or `imageFallbackLabel` and its number),\n * then the caption (WCAG 2.5.3 Label in Name).\n * @default false\n */\n readonly showCaptions = input(false);\n\n /** Whether clicking a thumbnail opens the lightbox */\n readonly enableLightbox = input(true);\n\n /** Gap between items in pixels */\n readonly gap = input(8);\n\n /** Aspect ratio for grid items (ignored in masonry mode) */\n readonly aspectRatio = input('1 / 1');\n\n /** Label of the \"all\" category filter (shown when the items span more than one category). */\n readonly filterAllLabel = input('All');\n\n /** Accessible name of the lightbox dialog. */\n readonly lightboxAriaLabel = input('Image lightbox');\n\n /** Accessible name of the lightbox zoom-out button. */\n readonly zoomOutAriaLabel = input('Zoom out');\n\n /** Accessible name of the lightbox zoom-in button. */\n readonly zoomInAriaLabel = input('Zoom in');\n\n /** Accessible name of the lightbox reset-zoom button. */\n readonly resetZoomAriaLabel = input('Reset zoom');\n\n /** Accessible name of the lightbox download button. */\n readonly downloadAriaLabel = input('Download');\n\n /** Accessible name of the lightbox close button. */\n readonly closeAriaLabel = input('Close');\n\n /** Accessible name of the previous-image button. */\n readonly previousAriaLabel = input('Previous image');\n\n /** Accessible name of the next-image button. */\n readonly nextAriaLabel = input('Next image');\n\n /**\n * Accessible name of a thumbnail without `alt`, followed by its number. A\n * shown caption follows (`Bild 3, Sonnenuntergang am See`).\n * @default 'Image'\n */\n readonly imageFallbackLabel = input('Image');\n\n /** Emitted when lightbox opens with item index */\n readonly lightboxOpen = output<number>();\n\n /** Emitted when lightbox closes */\n readonly lightboxClose = output<void>();\n\n // ── Internal state ───────────────────────────────────────────────────\n\n private readonly injector = inject(Injector);\n private readonly closeBtn = viewChild<ElementRef<HTMLButtonElement>>('closeBtn');\n /** Element that opened the lightbox — focus returns there on close. */\n private triggerElement: HTMLElement | null = null;\n\n protected lightboxIndex = signal<number | null>(null);\n protected lightboxZoom = signal(100);\n protected loadedImages = signal<Set<number>>(new Set());\n protected activeFilter = signal<string | null>(null);\n\n // ── Computed ─────────────────────────────────────────────────────────\n\n protected gridColumns = computed(() => {\n return this.columns() ?? SIZE_COLUMNS[this.size()];\n });\n\n protected gridStyle = computed(() => {\n const cols = this.gridColumns();\n const g = this.gap();\n if (this.layout() === 'masonry') {\n return {\n 'column-count': `${cols}`,\n 'column-gap': `${g}px`,\n };\n }\n return {\n 'display': 'grid',\n 'grid-template-columns': `repeat(${cols}, 1fr)`,\n 'gap': `${g}px`,\n };\n });\n\n protected itemStyle = computed(() => {\n if (this.layout() === 'masonry') return {};\n return { 'aspect-ratio': this.aspectRatio() };\n });\n\n protected categories = computed<string[]>(() => {\n const cats = new Set<string>();\n for (const item of this.items()) {\n if (item.category) cats.add(item.category);\n }\n return Array.from(cats).sort();\n });\n\n protected filteredItems = computed(() => {\n const filter = this.activeFilter();\n if (!filter) return this.items();\n return this.items().filter(item => item.category === filter);\n });\n\n protected showFilters = computed(() => this.categories().length > 1);\n\n protected lightboxItem = computed<GalleryItem | null>(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return null;\n const items = this.filteredItems();\n return items[idx] ?? null;\n });\n\n protected lightboxCounter = computed(() => {\n const idx = this.lightboxIndex();\n if (idx === null) return '';\n return `${idx + 1} / ${this.filteredItems().length}`;\n });\n\n protected lightboxTransform = computed(() => {\n const z = this.lightboxZoom() / 100;\n return `scale(${z})`;\n });\n\n protected canGoPrev = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx > 0;\n });\n\n protected canGoNext = computed(() => {\n const idx = this.lightboxIndex();\n return idx !== null && idx < this.filteredItems().length - 1;\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────\n\n ngOnDestroy(): void {\n // No focus restore here: the injector is going away with the view.\n this.closeLightbox(false);\n }\n\n // ── Actions ──────────────────────────────────────────────────────────\n\n /**\n * Opens the lightbox for `index`. Like a native button, Space activates on\n * *keyup* (keydown only prevents the page scroll): activating on keydown\n * would move focus to the Close button before the key is released, and the\n * Space keyup would then immediately click Close.\n */\n protected openLightbox(index: number, event?: Event): void {\n if (!this.enableLightbox()) return;\n if (event instanceof KeyboardEvent) event.preventDefault();\n this.triggerElement =\n (event?.currentTarget as HTMLElement | null) ??\n (document.activeElement instanceof HTMLElement ? document.activeElement : null);\n this.lightboxIndex.set(index);\n this.lightboxZoom.set(100);\n this.lightboxOpen.emit(index);\n document.body.style.overflow = 'hidden';\n // The dialog is `@if`-rendered — move focus in once it exists.\n afterNextRender(() => this.closeBtn()?.nativeElement.focus(), { injector: this.injector });\n }\n\n protected closeLightbox(restoreFocus = true): void {\n if (this.lightboxIndex() === null) return;\n this.lightboxIndex.set(null);\n this.lightboxZoom.set(100);\n this.lightboxClose.emit();\n document.body.style.overflow = '';\n const trigger = this.triggerElement;\n this.triggerElement = null;\n if (restoreFocus && trigger?.isConnected) {\n afterNextRender(() => trigger.focus(), { injector: this.injector });\n }\n }\n\n protected goToPrev(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx > 0) {\n this.lightboxIndex.set(idx - 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected goToNext(): void {\n const idx = this.lightboxIndex();\n if (idx !== null && idx < this.filteredItems().length - 1) {\n this.lightboxIndex.set(idx + 1);\n this.lightboxZoom.set(100);\n }\n }\n\n protected zoomIn(): void {\n this.lightboxZoom.update(z => Math.min(z + 25, 500));\n }\n\n protected zoomOut(): void {\n this.lightboxZoom.update(z => Math.max(z - 25, 25));\n }\n\n protected resetZoom(): void {\n this.lightboxZoom.set(100);\n }\n\n protected downloadCurrent(): void {\n const item = this.lightboxItem();\n // A programmatic click on a caller-supplied URL: only image-safe schemes.\n if (!item || !isSafeImageUrl(item.src)) return;\n const a = document.createElement('a');\n a.href = item.src;\n a.download = item.alt || 'image';\n a.target = '_blank';\n a.rel = 'noopener noreferrer';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }\n\n protected setFilter(category: string | null): void {\n this.activeFilter.set(category);\n }\n\n protected onImageLoad(index: number): void {\n this.loadedImages.update(set => {\n const next = new Set(set);\n next.add(index);\n return next;\n });\n }\n\n protected isLoaded(index: number): boolean {\n return this.loadedImages().has(index);\n }\n\n protected getThumbnail(item: GalleryItem): string {\n return item.thumbnail || item.src;\n }\n\n /**\n * Name of a thumbnail (AS-33, WCAG 2.5.3 Label in Name): `alt` (or\n * `imageFallbackLabel` and its number), then the shown caption unless one\n * contains the other.\n */\n protected itemAriaLabel(item: GalleryItem, index: number): string {\n const label = item.alt || `${this.imageFallbackLabel()} ${index + 1}`;\n return nameWithText(label, this.showCaptions() ? (item.caption ?? '') : '');\n }\n\n protected trackByIndex(index: number): number {\n return index;\n }\n\n /**\n * Keyboard handling while the lightbox is open. Bound on the lightbox itself\n * (focus is trapped inside it) and, as a fallback, on `document`. A handled\n * Escape stops propagating so an enclosing modal/drawer doesn't close too.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (this.lightboxIndex() === null) return;\n switch (event.key) {\n case 'Escape':\n event.stopPropagation();\n this.closeLightbox();\n break;\n case 'ArrowLeft':\n event.stopPropagation();\n this.goToPrev();\n break;\n case 'ArrowRight':\n event.stopPropagation();\n this.goToNext();\n break;\n }\n }\n}\n","<!-- Filters -->\n@if (showFilters()) {\n <div class=\"gallery__filters\">\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === null\"\n (click)=\"setFilter(null)\"\n >\n {{ filterAllLabel() }}\n </button>\n @for (cat of categories(); track cat) {\n <button\n type=\"button\"\n class=\"gallery__filter-btn\"\n [class.gallery__filter-btn--active]=\"activeFilter() === cat\"\n (click)=\"setFilter(cat)\"\n >\n {{ cat }}\n </button>\n }\n </div>\n}\n\n<!-- Grid / Masonry -->\n<div class=\"gallery__grid\" [class.gallery__grid--masonry]=\"layout() === 'masonry'\" [ngStyle]=\"gridStyle()\">\n @for (item of filteredItems(); track item.src; let i = $index) {\n <div\n class=\"gallery__item\"\n [class.gallery__item--loaded]=\"isLoaded(i)\"\n [class.gallery__item--clickable]=\"enableLightbox()\"\n [ngStyle]=\"itemStyle()\"\n (click)=\"openLightbox(i, $event)\"\n (keydown.enter)=\"openLightbox(i, $event)\"\n (keydown.space)=\"$event.preventDefault()\"\n (keyup.space)=\"openLightbox(i, $event)\"\n [attr.tabindex]=\"enableLightbox() ? 0 : null\"\n [attr.role]=\"enableLightbox() ? 'button' : null\"\n [attr.aria-label]=\"itemAriaLabel(item, i)\"\n >\n @if (!isLoaded(i)) {\n <div class=\"gallery__placeholder\"></div>\n }\n <img\n class=\"gallery__thumb\"\n [src]=\"getThumbnail(item)\"\n [alt]=\"item.alt || ''\"\n loading=\"lazy\"\n (load)=\"onImageLoad(i)\"\n />\n @if (showCaptions() && item.caption) {\n <div class=\"gallery__caption\">{{ item.caption }}</div>\n }\n </div>\n }\n</div>\n\n<!-- Lightbox overlay -->\n@if (lightboxItem(); as item) {\n <div\n class=\"gallery__lightbox\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"lightboxAriaLabel()\"\n tabindex=\"-1\"\n cdkTrapFocus\n (keydown)=\"onKeydown($event)\"\n >\n <!-- Backdrop -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- backdrop click is a mouse shortcut; Escape and the close button close the lightbox -->\n <div class=\"gallery__lightbox-backdrop\" (click)=\"closeLightbox()\"></div>\n\n <!-- Toolbar -->\n <div class=\"gallery__lightbox-toolbar\">\n <div class=\"gallery__lightbox-counter\">{{ lightboxCounter() }}</div>\n <div class=\"gallery__lightbox-actions\">\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomOutAriaLabel()\" (click)=\"zoomOut()\">\n <lc-icon name=\"minus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <span class=\"gallery__lightbox-zoom\">{{ lightboxZoom() }}%</span>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"zoomInAriaLabel()\" (click)=\"zoomIn()\">\n <lc-icon name=\"plus\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"resetZoomAriaLabel()\" (click)=\"resetZoom()\">\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" [decorative]=\"true\" />\n </button>\n <button type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"downloadAriaLabel()\" (click)=\"downloadCurrent()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" [decorative]=\"true\" />\n </button>\n <button #closeBtn type=\"button\" class=\"gallery__lightbox-btn\" [attr.aria-label]=\"closeAriaLabel()\" (click)=\"closeLightbox()\">\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </div>\n </div>\n\n <!-- Navigation -->\n @if (canGoPrev()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--prev\"\n [attr.aria-label]=\"previousAriaLabel()\"\n (click)=\"goToPrev()\"\n >\n <lc-icon name=\"chevron-left\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n @if (canGoNext()) {\n <button\n type=\"button\"\n class=\"gallery__lightbox-nav gallery__lightbox-nav--next\"\n [attr.aria-label]=\"nextAriaLabel()\"\n (click)=\"goToNext()\"\n >\n <lc-icon name=\"chevron-right\" size=\"lg\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Image -->\n <div class=\"gallery__lightbox-content\">\n <img\n class=\"gallery__lightbox-image\"\n [src]=\"item.src\"\n [alt]=\"item.alt || ''\"\n [style.transform]=\"lightboxTransform()\"\n />\n </div>\n\n <!-- Caption -->\n @if (item.caption) {\n <div class=\"gallery__lightbox-caption\">{{ item.caption }}</div>\n }\n </div>\n}\n"],"mappings":"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"}
|
|
@@ -219,7 +219,7 @@ var GanttChartComponent = class GanttChartComponent {
|
|
|
219
219
|
outputs: { taskClick: "taskClick" },
|
|
220
220
|
ngImport: i0,
|
|
221
221
|
template: "<div class=\"lc-gantt\">\n <div class=\"lc-gantt__container\">\n <!-- Left: Task labels -->\n <div class=\"lc-gantt__labels\" [style.width.px]=\"labelWidth()\">\n <div class=\"lc-gantt__labels-header\">{{ labelsHeader() }}</div>\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__label-row\" [style.height.px]=\"rowHeight()\">\n <span class=\"lc-gantt__label-text\" [title]=\"bar.task.title\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n\n <!-- Right: Timeline -->\n <div class=\"lc-gantt__timeline-wrapper\">\n <!-- Month headers -->\n <div class=\"lc-gantt__month-row\" [style.width.px]=\"timelineWidth()\">\n @for (mh of monthHeaders(); track mh.label + mh.left) {\n <div class=\"lc-gantt__month-cell\" [style.left.px]=\"mh.left\" [style.width.px]=\"mh.width\">\n {{ mh.label }}\n </div>\n }\n </div>\n\n <!-- Day headers -->\n <div class=\"lc-gantt__day-row\" [style.width.px]=\"timelineWidth()\">\n @for (col of dayColumns(); track col.left) {\n <div\n class=\"lc-gantt__day-cell\"\n [class.lc-gantt__day-cell--weekend]=\"col.isWeekend\"\n [style.left.px]=\"col.left\"\n [style.width.px]=\"dayWidth()\"\n >\n {{ col.label }}\n </div>\n }\n </div>\n\n <!-- Grid + Bars -->\n <div class=\"lc-gantt__grid\" [style.width.px]=\"timelineWidth()\" [style.height.px]=\"gridHeight()\">\n <!-- Weekend columns -->\n @for (col of dayColumns(); track col.left) {\n @if (col.isWeekend) {\n <div class=\"lc-gantt__weekend-bg\" [style.left.px]=\"col.left\" [style.width.px]=\"dayWidth()\" [style.height.px]=\"gridHeight()\"></div>\n }\n }\n\n <!-- Row borders -->\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__row-line\" [style.top.px]=\"bar.top + rowHeight()\"></div>\n }\n\n <!-- Today marker -->\n @if (todayLeft() >= 0) {\n <div class=\"lc-gantt__today\" [style.left.px]=\"todayLeft()\" [style.height.px]=\"gridHeight()\"></div>\n }\n\n <!-- Dependency lines SVG -->\n @if (depLines().length) {\n <svg class=\"lc-gantt__dep-svg\" [attr.width]=\"timelineWidth()\" [attr.height]=\"gridHeight()\">\n <defs>\n <marker [attr.id]=\"markerId\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L8,3 L0,6 Z\" class=\"lc-gantt__arrow\" />\n </marker>\n </defs>\n @for (dep of depLines(); track $index) {\n <path\n [attr.d]=\"'M' + dep.x1 + ',' + dep.y1 + ' C' + (dep.x1 + 20) + ',' + dep.y1 + ' ' + (dep.x2 - 20) + ',' + dep.y2 + ' ' + dep.x2 + ',' + dep.y2\"\n class=\"lc-gantt__dep-line\"\n [attr.marker-end]=\"'url(#' + markerId + ')'\"\n />\n }\n </svg>\n }\n\n <!-- Task bars -->\n @for (bar of bars(); track bar.task.id) {\n <div\n [class]=\"'lc-gantt__bar lc-gantt__bar--' + bar.colorClass\"\n role=\"button\"\n tabindex=\"0\"\n [style.left.px]=\"bar.left\"\n [style.top.px]=\"bar.top + 8\"\n [style.width.px]=\"bar.width\"\n [style.height.px]=\"rowHeight() - 16\"\n [title]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n [attr.aria-label]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n (click)=\"onTaskClick(bar.task)\"\n (keydown)=\"onTaskKeydown($event, bar.task)\"\n >\n @if (bar.progress > 0) {\n <div class=\"lc-gantt__bar-progress\" [style.width.%]=\"bar.progress\"></div>\n }\n <span class=\"lc-gantt__bar-label\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n",
|
|
222
|
-
styles: [":host{display:block}.lc-gantt{font-family:var(--typography-font-family, sans-serif);border:1px solid var(--color-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface)}.lc-gantt__container{display:flex;overflow:auto}.lc-gantt__labels{flex-shrink:0;border-right:2px solid var(--color-border);background:var(--color-surface-2);position:sticky;left:0;z-index:3}.lc-gantt__labels-header{height:3.5rem;display:flex;align-items:center;padding:0 .75rem;font-size:.75rem;font-weight:600;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.025em;border-bottom:1px solid var(--color-border)}.lc-gantt__label-row{display:flex;align-items:center;padding:0 .75rem;border-bottom:1px solid var(--color-divider)}.lc-gantt__label-text{font-size:.8125rem;font-weight:500;color:var(--color-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__timeline-wrapper{flex:1;overflow:auto}.lc-gantt__month-row{display:flex;position:relative;height:1.5rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__month-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;padding-left:.5rem;font-size:.6875rem;font-weight:600;color:var(--color-text-secondary);border-left:1px solid var(--color-border)}.lc-gantt__day-row{display:flex;position:relative;height:2rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__day-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;justify-content:center;font-size:.625rem;color:var(--color-text-secondary);border-left:1px solid var(--color-divider)}.lc-gantt__day-cell--weekend{color:var(--color-text-tertiary);background:color-mix(in srgb,var(--color-border) 30%,transparent)}.lc-gantt__grid{position:relative;min-height:4rem}.lc-gantt__weekend-bg{position:absolute;top:0;background:color-mix(in srgb,var(--color-border) 20%,transparent)}.lc-gantt__row-line{position:absolute;left:0;right:0;height:1px;background:var(--color-divider)}.lc-gantt__today{position:absolute;top:0;width:2px;background:var(--color-error-default);z-index:2;opacity:.7}.lc-gantt__bar{position:absolute;border-radius:var(--border-radius-sm);cursor:pointer;overflow:hidden;display:flex;align-items:center;min-width:.25rem;transition:filter .15s}.lc-gantt__bar:hover{filter:brightness(1.1)}.lc-gantt__bar:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500);z-index:2}.lc-gantt__bar--primary{background:var(--color-primary-500)}.lc-gantt__bar--secondary{background:var(--color-secondary-500)}.lc-gantt__bar--success{background:var(--color-success-default)}.lc-gantt__bar--warning{background:var(--color-warning-default)}.lc-gantt__bar--error{background:var(--color-error-default)}.lc-gantt__bar--info{background:var(--color-info-default)}.lc-gantt__bar-progress{position:absolute;top:0;left:0;height:100%;background:#00000026;border-radius:.25rem 0 0 .25rem}.lc-gantt__bar-label{position:relative;z-index:1;padding:0 .375rem;font-size:.6875rem;font-weight:500;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__dep-svg{position:absolute;top:0;left:0;pointer-events:none;z-index:1}.lc-gantt__dep-line{fill:none;stroke:var(--color-border-strong);stroke-width:1.5}.lc-gantt__arrow{fill:var(--color-text-tertiary)}\n"],
|
|
222
|
+
styles: [":host{display:block}.lc-gantt{font-family:var(--typography-font-family, sans-serif);border:1px solid var(--color-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface)}.lc-gantt__container{display:flex;overflow:auto}.lc-gantt__labels{flex-shrink:0;border-right:2px solid var(--color-border);background:var(--color-surface-2);position:sticky;left:0;z-index:3}.lc-gantt__labels-header{height:3.5rem;display:flex;align-items:center;padding:0 .75rem;font-size:.75rem;font-weight:600;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.025em;border-bottom:1px solid var(--color-border)}.lc-gantt__label-row{display:flex;align-items:center;padding:0 .75rem;border-bottom:1px solid var(--color-divider)}.lc-gantt__label-text{font-size:.8125rem;font-weight:500;color:var(--color-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__timeline-wrapper{flex:1;overflow:auto}.lc-gantt__month-row{display:flex;position:relative;height:1.5rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__month-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;padding-left:.5rem;font-size:.6875rem;font-weight:600;color:var(--color-text-secondary);border-left:1px solid var(--color-border)}.lc-gantt__day-row{display:flex;position:relative;height:2rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__day-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;justify-content:center;font-size:.625rem;color:var(--color-text-secondary);border-left:1px solid var(--color-divider)}.lc-gantt__day-cell--weekend{color:var(--color-text-tertiary);background:color-mix(in srgb,var(--color-border) 30%,transparent)}.lc-gantt__grid{position:relative;min-height:4rem}.lc-gantt__weekend-bg{position:absolute;top:0;background:color-mix(in srgb,var(--color-border) 20%,transparent)}.lc-gantt__row-line{position:absolute;left:0;right:0;height:1px;background:var(--color-divider)}.lc-gantt__today{position:absolute;top:0;width:2px;background:var(--color-error-default);z-index:2;opacity:.7}.lc-gantt__bar{position:absolute;border-radius:var(--border-radius-sm);cursor:pointer;overflow:hidden;display:flex;align-items:center;min-width:.25rem;transition:filter .15s}.lc-gantt__bar:hover{filter:brightness(1.1)}.lc-gantt__bar:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500);z-index:2}.lc-gantt__bar--primary{background:var(--color-primary-500)}.lc-gantt__bar--secondary{background:var(--color-secondary-500)}.lc-gantt__bar--success{background:var(--color-success-default)}.lc-gantt__bar--warning{background:var(--color-warning-default)}.lc-gantt__bar--error{background:var(--color-error-default)}.lc-gantt__bar--info{background:var(--color-info-default)}.lc-gantt__bar-progress{position:absolute;top:0;left:0;height:100%;background:#00000026;border-radius:.25rem 0 0 .25rem}.lc-gantt__bar-label{position:relative;z-index:1;padding:0 .375rem;font-size:.6875rem;font-weight:500;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__dep-svg{position:absolute;top:0;left:0;pointer-events:none;z-index:1}.lc-gantt__dep-line{fill:none;stroke:var(--color-border-strong);stroke-width:1.5}.lc-gantt__arrow{fill:var(--color-text-tertiary)}@media(forced-colors:active){.lc-gantt__bar:focus-visible{outline:2px solid Highlight;outline-offset:2px}}\n"],
|
|
223
223
|
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
224
224
|
});
|
|
225
225
|
};
|
|
@@ -235,7 +235,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
235
235
|
standalone: true,
|
|
236
236
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
237
237
|
template: "<div class=\"lc-gantt\">\n <div class=\"lc-gantt__container\">\n <!-- Left: Task labels -->\n <div class=\"lc-gantt__labels\" [style.width.px]=\"labelWidth()\">\n <div class=\"lc-gantt__labels-header\">{{ labelsHeader() }}</div>\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__label-row\" [style.height.px]=\"rowHeight()\">\n <span class=\"lc-gantt__label-text\" [title]=\"bar.task.title\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n\n <!-- Right: Timeline -->\n <div class=\"lc-gantt__timeline-wrapper\">\n <!-- Month headers -->\n <div class=\"lc-gantt__month-row\" [style.width.px]=\"timelineWidth()\">\n @for (mh of monthHeaders(); track mh.label + mh.left) {\n <div class=\"lc-gantt__month-cell\" [style.left.px]=\"mh.left\" [style.width.px]=\"mh.width\">\n {{ mh.label }}\n </div>\n }\n </div>\n\n <!-- Day headers -->\n <div class=\"lc-gantt__day-row\" [style.width.px]=\"timelineWidth()\">\n @for (col of dayColumns(); track col.left) {\n <div\n class=\"lc-gantt__day-cell\"\n [class.lc-gantt__day-cell--weekend]=\"col.isWeekend\"\n [style.left.px]=\"col.left\"\n [style.width.px]=\"dayWidth()\"\n >\n {{ col.label }}\n </div>\n }\n </div>\n\n <!-- Grid + Bars -->\n <div class=\"lc-gantt__grid\" [style.width.px]=\"timelineWidth()\" [style.height.px]=\"gridHeight()\">\n <!-- Weekend columns -->\n @for (col of dayColumns(); track col.left) {\n @if (col.isWeekend) {\n <div class=\"lc-gantt__weekend-bg\" [style.left.px]=\"col.left\" [style.width.px]=\"dayWidth()\" [style.height.px]=\"gridHeight()\"></div>\n }\n }\n\n <!-- Row borders -->\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__row-line\" [style.top.px]=\"bar.top + rowHeight()\"></div>\n }\n\n <!-- Today marker -->\n @if (todayLeft() >= 0) {\n <div class=\"lc-gantt__today\" [style.left.px]=\"todayLeft()\" [style.height.px]=\"gridHeight()\"></div>\n }\n\n <!-- Dependency lines SVG -->\n @if (depLines().length) {\n <svg class=\"lc-gantt__dep-svg\" [attr.width]=\"timelineWidth()\" [attr.height]=\"gridHeight()\">\n <defs>\n <marker [attr.id]=\"markerId\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L8,3 L0,6 Z\" class=\"lc-gantt__arrow\" />\n </marker>\n </defs>\n @for (dep of depLines(); track $index) {\n <path\n [attr.d]=\"'M' + dep.x1 + ',' + dep.y1 + ' C' + (dep.x1 + 20) + ',' + dep.y1 + ' ' + (dep.x2 - 20) + ',' + dep.y2 + ' ' + dep.x2 + ',' + dep.y2\"\n class=\"lc-gantt__dep-line\"\n [attr.marker-end]=\"'url(#' + markerId + ')'\"\n />\n }\n </svg>\n }\n\n <!-- Task bars -->\n @for (bar of bars(); track bar.task.id) {\n <div\n [class]=\"'lc-gantt__bar lc-gantt__bar--' + bar.colorClass\"\n role=\"button\"\n tabindex=\"0\"\n [style.left.px]=\"bar.left\"\n [style.top.px]=\"bar.top + 8\"\n [style.width.px]=\"bar.width\"\n [style.height.px]=\"rowHeight() - 16\"\n [title]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n [attr.aria-label]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n (click)=\"onTaskClick(bar.task)\"\n (keydown)=\"onTaskKeydown($event, bar.task)\"\n >\n @if (bar.progress > 0) {\n <div class=\"lc-gantt__bar-progress\" [style.width.%]=\"bar.progress\"></div>\n }\n <span class=\"lc-gantt__bar-label\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n",
|
|
238
|
-
styles: [":host{display:block}.lc-gantt{font-family:var(--typography-font-family, sans-serif);border:1px solid var(--color-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface)}.lc-gantt__container{display:flex;overflow:auto}.lc-gantt__labels{flex-shrink:0;border-right:2px solid var(--color-border);background:var(--color-surface-2);position:sticky;left:0;z-index:3}.lc-gantt__labels-header{height:3.5rem;display:flex;align-items:center;padding:0 .75rem;font-size:.75rem;font-weight:600;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.025em;border-bottom:1px solid var(--color-border)}.lc-gantt__label-row{display:flex;align-items:center;padding:0 .75rem;border-bottom:1px solid var(--color-divider)}.lc-gantt__label-text{font-size:.8125rem;font-weight:500;color:var(--color-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__timeline-wrapper{flex:1;overflow:auto}.lc-gantt__month-row{display:flex;position:relative;height:1.5rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__month-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;padding-left:.5rem;font-size:.6875rem;font-weight:600;color:var(--color-text-secondary);border-left:1px solid var(--color-border)}.lc-gantt__day-row{display:flex;position:relative;height:2rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__day-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;justify-content:center;font-size:.625rem;color:var(--color-text-secondary);border-left:1px solid var(--color-divider)}.lc-gantt__day-cell--weekend{color:var(--color-text-tertiary);background:color-mix(in srgb,var(--color-border) 30%,transparent)}.lc-gantt__grid{position:relative;min-height:4rem}.lc-gantt__weekend-bg{position:absolute;top:0;background:color-mix(in srgb,var(--color-border) 20%,transparent)}.lc-gantt__row-line{position:absolute;left:0;right:0;height:1px;background:var(--color-divider)}.lc-gantt__today{position:absolute;top:0;width:2px;background:var(--color-error-default);z-index:2;opacity:.7}.lc-gantt__bar{position:absolute;border-radius:var(--border-radius-sm);cursor:pointer;overflow:hidden;display:flex;align-items:center;min-width:.25rem;transition:filter .15s}.lc-gantt__bar:hover{filter:brightness(1.1)}.lc-gantt__bar:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500);z-index:2}.lc-gantt__bar--primary{background:var(--color-primary-500)}.lc-gantt__bar--secondary{background:var(--color-secondary-500)}.lc-gantt__bar--success{background:var(--color-success-default)}.lc-gantt__bar--warning{background:var(--color-warning-default)}.lc-gantt__bar--error{background:var(--color-error-default)}.lc-gantt__bar--info{background:var(--color-info-default)}.lc-gantt__bar-progress{position:absolute;top:0;left:0;height:100%;background:#00000026;border-radius:.25rem 0 0 .25rem}.lc-gantt__bar-label{position:relative;z-index:1;padding:0 .375rem;font-size:.6875rem;font-weight:500;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__dep-svg{position:absolute;top:0;left:0;pointer-events:none;z-index:1}.lc-gantt__dep-line{fill:none;stroke:var(--color-border-strong);stroke-width:1.5}.lc-gantt__arrow{fill:var(--color-text-tertiary)}\n"]
|
|
238
|
+
styles: [":host{display:block}.lc-gantt{font-family:var(--typography-font-family, sans-serif);border:1px solid var(--color-border);border-radius:var(--border-radius-md);overflow:hidden;background:var(--color-surface)}.lc-gantt__container{display:flex;overflow:auto}.lc-gantt__labels{flex-shrink:0;border-right:2px solid var(--color-border);background:var(--color-surface-2);position:sticky;left:0;z-index:3}.lc-gantt__labels-header{height:3.5rem;display:flex;align-items:center;padding:0 .75rem;font-size:.75rem;font-weight:600;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.025em;border-bottom:1px solid var(--color-border)}.lc-gantt__label-row{display:flex;align-items:center;padding:0 .75rem;border-bottom:1px solid var(--color-divider)}.lc-gantt__label-text{font-size:.8125rem;font-weight:500;color:var(--color-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__timeline-wrapper{flex:1;overflow:auto}.lc-gantt__month-row{display:flex;position:relative;height:1.5rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__month-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;padding-left:.5rem;font-size:.6875rem;font-weight:600;color:var(--color-text-secondary);border-left:1px solid var(--color-border)}.lc-gantt__day-row{display:flex;position:relative;height:2rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-gantt__day-cell{position:absolute;top:0;height:100%;display:flex;align-items:center;justify-content:center;font-size:.625rem;color:var(--color-text-secondary);border-left:1px solid var(--color-divider)}.lc-gantt__day-cell--weekend{color:var(--color-text-tertiary);background:color-mix(in srgb,var(--color-border) 30%,transparent)}.lc-gantt__grid{position:relative;min-height:4rem}.lc-gantt__weekend-bg{position:absolute;top:0;background:color-mix(in srgb,var(--color-border) 20%,transparent)}.lc-gantt__row-line{position:absolute;left:0;right:0;height:1px;background:var(--color-divider)}.lc-gantt__today{position:absolute;top:0;width:2px;background:var(--color-error-default);z-index:2;opacity:.7}.lc-gantt__bar{position:absolute;border-radius:var(--border-radius-sm);cursor:pointer;overflow:hidden;display:flex;align-items:center;min-width:.25rem;transition:filter .15s}.lc-gantt__bar:hover{filter:brightness(1.1)}.lc-gantt__bar:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500);z-index:2}.lc-gantt__bar--primary{background:var(--color-primary-500)}.lc-gantt__bar--secondary{background:var(--color-secondary-500)}.lc-gantt__bar--success{background:var(--color-success-default)}.lc-gantt__bar--warning{background:var(--color-warning-default)}.lc-gantt__bar--error{background:var(--color-error-default)}.lc-gantt__bar--info{background:var(--color-info-default)}.lc-gantt__bar-progress{position:absolute;top:0;left:0;height:100%;background:#00000026;border-radius:.25rem 0 0 .25rem}.lc-gantt__bar-label{position:relative;z-index:1;padding:0 .375rem;font-size:.6875rem;font-weight:500;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lc-gantt__dep-svg{position:absolute;top:0;left:0;pointer-events:none;z-index:1}.lc-gantt__dep-line{fill:none;stroke:var(--color-border-strong);stroke-width:1.5}.lc-gantt__arrow{fill:var(--color-text-tertiary)}@media(forced-colors:active){.lc-gantt__bar:focus-visible{outline:2px solid Highlight;outline-offset:2px}}\n"]
|
|
239
239
|
}]
|
|
240
240
|
}],
|
|
241
241
|
propDecorators: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-gantt-chart.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/gantt-chart/gantt-chart.component.ts","../../../../libs/angular-ui-kit/src/lib/gantt-chart/gantt-chart.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n output,\n computed,\n} from '@angular/core';\n\nexport interface GanttDependency {\n from: string;\n to: string;\n}\n\nexport interface GanttTask {\n id: string;\n title: string;\n start: Date | string;\n end: Date | string;\n progress?: number;\n color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';\n group?: string;\n}\n\ninterface NormalizedTask {\n id: string;\n title: string;\n start: Date;\n end: Date;\n progress: number;\n color: string;\n group: string;\n}\n\ninterface BarLayout {\n task: NormalizedTask;\n left: number;\n width: number;\n top: number;\n progress: number;\n colorClass: string;\n}\n\ninterface DepLine {\n x1: number;\n y1: number;\n x2: number;\n y2: number;\n}\n\n@Component({\n selector: 'lc-gantt-chart',\n standalone: true,\n templateUrl: './gantt-chart.component.html',\n styleUrls: ['./gantt-chart.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Gantt chart component for project timeline visualization.\n *\n * Features:\n * - Task bars with progress indicators\n * - Dependency lines with Bézier curves and arrowheads\n * - Monthly and daily header timeline\n * - Today marker line\n * - Weekend highlighting\n * - Configurable row height, label width, day width, locale and label header\n * - Task click event handling (bars are focusable, Enter / Space activate)\n *\n * @example\n * ```html\n * <lc-gantt-chart [tasks]=\"tasks\" [dependencies]=\"deps\" [showToday]=\"true\" />\n * ```\n */\nexport class GanttChartComponent {\n private static nextId = 0;\n\n /**\n * Per-instance id for the SVG arrowhead marker. `<marker id>` is document\n * global, so two charts on one page must not share it.\n */\n protected readonly markerId = `lc-gantt-arrowhead-${++GanttChartComponent.nextId}`;\n\n /** Tasks to display. */\n tasks = input.required<GanttTask[]>();\n\n /** Dependency links between tasks. */\n dependencies = input<GanttDependency[]>([]);\n\n /** Row height in px. */\n rowHeight = input<number>(40);\n\n /** Width of the label column in px. */\n labelWidth = input<number>(200);\n\n /** Column width per day in px. */\n dayWidth = input<number>(36);\n\n /** Show today marker. */\n showToday = input<boolean>(true);\n\n /** BCP 47 locale used for the month header labels. */\n locale = input<string>('de-DE');\n\n /** Header text of the task label column. */\n labelsHeader = input<string>('Aufgaben');\n\n /** Emits when a task bar is clicked / activated via keyboard. */\n taskClick = output<GanttTask>();\n\n private readonly normalizedTasks = computed((): NormalizedTask[] =>\n this.tasks().map(t => ({\n id: t.id,\n title: t.title,\n start: t.start instanceof Date ? t.start : new Date(t.start),\n end: t.end instanceof Date ? t.end : new Date(t.end),\n progress: t.progress ?? 0,\n color: t.color ?? 'primary',\n group: t.group ?? '',\n }))\n );\n\n protected readonly timelineStart = computed(() => {\n const tasks = this.normalizedTasks();\n if (!tasks.length) return new Date();\n const min = Math.min(...tasks.map(t => t.start.getTime()));\n const d = new Date(min);\n d.setDate(d.getDate() - 2);\n return this.startOfDay(d);\n });\n\n protected readonly timelineEnd = computed(() => {\n const tasks = this.normalizedTasks();\n if (!tasks.length) return new Date();\n const max = Math.max(...tasks.map(t => t.end.getTime()));\n const d = new Date(max);\n d.setDate(d.getDate() + 3);\n return this.startOfDay(d);\n });\n\n protected readonly totalDays = computed(() => {\n const start = this.timelineStart();\n const end = this.timelineEnd();\n return Math.ceil((end.getTime() - start.getTime()) / 86400000) + 1;\n });\n\n protected readonly timelineWidth = computed(() => this.totalDays() * this.dayWidth());\n\n protected readonly dayColumns = computed(() => {\n const start = this.timelineStart();\n const days = this.totalDays();\n const dw = this.dayWidth();\n return Array.from({ length: days }, (_, i) => {\n const d = new Date(start);\n d.setDate(d.getDate() + i);\n return {\n date: d,\n left: i * dw,\n label: d.getDate(),\n isWeekend: d.getDay() === 0 || d.getDay() === 6,\n monthLabel: d.getDate() === 1 ? d.toLocaleDateString(this.locale(), { month: 'short' }) : null,\n };\n });\n });\n\n protected readonly monthHeaders = computed(() => {\n const cols = this.dayColumns();\n const dw = this.dayWidth();\n const locale = this.locale();\n const months: { label: string; left: number; width: number }[] = [];\n let current: { label: string; left: number; count: number } | null = null;\n\n for (const col of cols) {\n const label = col.date.toLocaleDateString(locale, { month: 'short', year: 'numeric' });\n if (!current || current.label !== label) {\n if (current) months.push({ label: current.label, left: current.left, width: current.count * dw });\n current = { label, left: col.left, count: 1 };\n } else {\n current.count++;\n }\n }\n if (current) months.push({ label: current.label, left: current.left, width: current.count * dw });\n return months;\n });\n\n protected readonly bars = computed((): BarLayout[] => {\n const tasks = this.normalizedTasks();\n const start = this.timelineStart().getTime();\n const dw = this.dayWidth();\n const rh = this.rowHeight();\n\n return tasks.map((t, i) => {\n const dayOffset = (t.start.getTime() - start) / 86400000;\n const duration = Math.max((t.end.getTime() - t.start.getTime()) / 86400000, 1);\n return {\n task: t,\n left: dayOffset * dw,\n width: duration * dw,\n top: i * rh,\n progress: Math.min(Math.max(t.progress, 0), 100),\n colorClass: t.color,\n };\n });\n });\n\n protected readonly depLines = computed((): DepLine[] => {\n const deps = this.dependencies();\n const barsMap = new Map<string, BarLayout>();\n for (const bar of this.bars()) barsMap.set(bar.task.id, bar);\n const rh = this.rowHeight();\n\n return deps\n .filter(d => barsMap.has(d.from) && barsMap.has(d.to))\n .map(d => {\n const from = barsMap.get(d.from)!;\n const to = barsMap.get(d.to)!;\n return {\n x1: from.left + from.width,\n y1: from.top + rh / 2,\n x2: to.left,\n y2: to.top + rh / 2,\n };\n });\n });\n\n protected readonly todayLeft = computed(() => {\n if (!this.showToday()) return -1;\n const start = this.timelineStart().getTime();\n const now = this.startOfDay(new Date()).getTime();\n const dayOffset = (now - start) / 86400000;\n if (dayOffset < 0 || dayOffset > this.totalDays()) return -1;\n return dayOffset * this.dayWidth();\n });\n\n protected readonly gridHeight = computed(() => this.normalizedTasks().length * this.rowHeight());\n\n protected onTaskClick(task: NormalizedTask): void {\n this.taskClick.emit(this.tasks().find(t => t.id === task.id)!);\n }\n\n protected onTaskKeydown(event: KeyboardEvent, task: NormalizedTask): void {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.onTaskClick(task);\n }\n }\n\n private startOfDay(d: Date): Date {\n return new Date(d.getFullYear(), d.getMonth(), d.getDate());\n }\n}\n","<div class=\"lc-gantt\">\n <div class=\"lc-gantt__container\">\n <!-- Left: Task labels -->\n <div class=\"lc-gantt__labels\" [style.width.px]=\"labelWidth()\">\n <div class=\"lc-gantt__labels-header\">{{ labelsHeader() }}</div>\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__label-row\" [style.height.px]=\"rowHeight()\">\n <span class=\"lc-gantt__label-text\" [title]=\"bar.task.title\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n\n <!-- Right: Timeline -->\n <div class=\"lc-gantt__timeline-wrapper\">\n <!-- Month headers -->\n <div class=\"lc-gantt__month-row\" [style.width.px]=\"timelineWidth()\">\n @for (mh of monthHeaders(); track mh.label + mh.left) {\n <div class=\"lc-gantt__month-cell\" [style.left.px]=\"mh.left\" [style.width.px]=\"mh.width\">\n {{ mh.label }}\n </div>\n }\n </div>\n\n <!-- Day headers -->\n <div class=\"lc-gantt__day-row\" [style.width.px]=\"timelineWidth()\">\n @for (col of dayColumns(); track col.left) {\n <div\n class=\"lc-gantt__day-cell\"\n [class.lc-gantt__day-cell--weekend]=\"col.isWeekend\"\n [style.left.px]=\"col.left\"\n [style.width.px]=\"dayWidth()\"\n >\n {{ col.label }}\n </div>\n }\n </div>\n\n <!-- Grid + Bars -->\n <div class=\"lc-gantt__grid\" [style.width.px]=\"timelineWidth()\" [style.height.px]=\"gridHeight()\">\n <!-- Weekend columns -->\n @for (col of dayColumns(); track col.left) {\n @if (col.isWeekend) {\n <div class=\"lc-gantt__weekend-bg\" [style.left.px]=\"col.left\" [style.width.px]=\"dayWidth()\" [style.height.px]=\"gridHeight()\"></div>\n }\n }\n\n <!-- Row borders -->\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__row-line\" [style.top.px]=\"bar.top + rowHeight()\"></div>\n }\n\n <!-- Today marker -->\n @if (todayLeft() >= 0) {\n <div class=\"lc-gantt__today\" [style.left.px]=\"todayLeft()\" [style.height.px]=\"gridHeight()\"></div>\n }\n\n <!-- Dependency lines SVG -->\n @if (depLines().length) {\n <svg class=\"lc-gantt__dep-svg\" [attr.width]=\"timelineWidth()\" [attr.height]=\"gridHeight()\">\n <defs>\n <marker [attr.id]=\"markerId\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L8,3 L0,6 Z\" class=\"lc-gantt__arrow\" />\n </marker>\n </defs>\n @for (dep of depLines(); track $index) {\n <path\n [attr.d]=\"'M' + dep.x1 + ',' + dep.y1 + ' C' + (dep.x1 + 20) + ',' + dep.y1 + ' ' + (dep.x2 - 20) + ',' + dep.y2 + ' ' + dep.x2 + ',' + dep.y2\"\n class=\"lc-gantt__dep-line\"\n [attr.marker-end]=\"'url(#' + markerId + ')'\"\n />\n }\n </svg>\n }\n\n <!-- Task bars -->\n @for (bar of bars(); track bar.task.id) {\n <div\n [class]=\"'lc-gantt__bar lc-gantt__bar--' + bar.colorClass\"\n role=\"button\"\n tabindex=\"0\"\n [style.left.px]=\"bar.left\"\n [style.top.px]=\"bar.top + 8\"\n [style.width.px]=\"bar.width\"\n [style.height.px]=\"rowHeight() - 16\"\n [title]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n [attr.aria-label]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n (click)=\"onTaskClick(bar.task)\"\n (keydown)=\"onTaskKeydown($event, bar.task)\"\n >\n @if (bar.progress > 0) {\n <div class=\"lc-gantt__bar-progress\" [style.width.%]=\"bar.progress\"></div>\n }\n <span class=\"lc-gantt__bar-label\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-gantt-chart.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/gantt-chart/gantt-chart.component.ts","../../../../libs/angular-ui-kit/src/lib/gantt-chart/gantt-chart.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n output,\n computed,\n} from '@angular/core';\n\nexport interface GanttDependency {\n from: string;\n to: string;\n}\n\nexport interface GanttTask {\n id: string;\n title: string;\n start: Date | string;\n end: Date | string;\n progress?: number;\n color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';\n group?: string;\n}\n\ninterface NormalizedTask {\n id: string;\n title: string;\n start: Date;\n end: Date;\n progress: number;\n color: string;\n group: string;\n}\n\ninterface BarLayout {\n task: NormalizedTask;\n left: number;\n width: number;\n top: number;\n progress: number;\n colorClass: string;\n}\n\ninterface DepLine {\n x1: number;\n y1: number;\n x2: number;\n y2: number;\n}\n\n@Component({\n selector: 'lc-gantt-chart',\n standalone: true,\n templateUrl: './gantt-chart.component.html',\n styleUrls: ['./gantt-chart.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Gantt chart component for project timeline visualization.\n *\n * Features:\n * - Task bars with progress indicators\n * - Dependency lines with Bézier curves and arrowheads\n * - Monthly and daily header timeline\n * - Today marker line\n * - Weekend highlighting\n * - Configurable row height, label width, day width, locale and label header\n * - Task click event handling (bars are focusable, Enter / Space activate)\n *\n * @example\n * ```html\n * <lc-gantt-chart [tasks]=\"tasks\" [dependencies]=\"deps\" [showToday]=\"true\" />\n * ```\n */\nexport class GanttChartComponent {\n private static nextId = 0;\n\n /**\n * Per-instance id for the SVG arrowhead marker. `<marker id>` is document\n * global, so two charts on one page must not share it.\n */\n protected readonly markerId = `lc-gantt-arrowhead-${++GanttChartComponent.nextId}`;\n\n /** Tasks to display. */\n tasks = input.required<GanttTask[]>();\n\n /** Dependency links between tasks. */\n dependencies = input<GanttDependency[]>([]);\n\n /** Row height in px. */\n rowHeight = input<number>(40);\n\n /** Width of the label column in px. */\n labelWidth = input<number>(200);\n\n /** Column width per day in px. */\n dayWidth = input<number>(36);\n\n /** Show today marker. */\n showToday = input<boolean>(true);\n\n /** BCP 47 locale used for the month header labels. */\n locale = input<string>('de-DE');\n\n /** Header text of the task label column. */\n labelsHeader = input<string>('Aufgaben');\n\n /** Emits when a task bar is clicked / activated via keyboard. */\n taskClick = output<GanttTask>();\n\n private readonly normalizedTasks = computed((): NormalizedTask[] =>\n this.tasks().map(t => ({\n id: t.id,\n title: t.title,\n start: t.start instanceof Date ? t.start : new Date(t.start),\n end: t.end instanceof Date ? t.end : new Date(t.end),\n progress: t.progress ?? 0,\n color: t.color ?? 'primary',\n group: t.group ?? '',\n }))\n );\n\n protected readonly timelineStart = computed(() => {\n const tasks = this.normalizedTasks();\n if (!tasks.length) return new Date();\n const min = Math.min(...tasks.map(t => t.start.getTime()));\n const d = new Date(min);\n d.setDate(d.getDate() - 2);\n return this.startOfDay(d);\n });\n\n protected readonly timelineEnd = computed(() => {\n const tasks = this.normalizedTasks();\n if (!tasks.length) return new Date();\n const max = Math.max(...tasks.map(t => t.end.getTime()));\n const d = new Date(max);\n d.setDate(d.getDate() + 3);\n return this.startOfDay(d);\n });\n\n protected readonly totalDays = computed(() => {\n const start = this.timelineStart();\n const end = this.timelineEnd();\n return Math.ceil((end.getTime() - start.getTime()) / 86400000) + 1;\n });\n\n protected readonly timelineWidth = computed(() => this.totalDays() * this.dayWidth());\n\n protected readonly dayColumns = computed(() => {\n const start = this.timelineStart();\n const days = this.totalDays();\n const dw = this.dayWidth();\n return Array.from({ length: days }, (_, i) => {\n const d = new Date(start);\n d.setDate(d.getDate() + i);\n return {\n date: d,\n left: i * dw,\n label: d.getDate(),\n isWeekend: d.getDay() === 0 || d.getDay() === 6,\n monthLabel: d.getDate() === 1 ? d.toLocaleDateString(this.locale(), { month: 'short' }) : null,\n };\n });\n });\n\n protected readonly monthHeaders = computed(() => {\n const cols = this.dayColumns();\n const dw = this.dayWidth();\n const locale = this.locale();\n const months: { label: string; left: number; width: number }[] = [];\n let current: { label: string; left: number; count: number } | null = null;\n\n for (const col of cols) {\n const label = col.date.toLocaleDateString(locale, { month: 'short', year: 'numeric' });\n if (!current || current.label !== label) {\n if (current) months.push({ label: current.label, left: current.left, width: current.count * dw });\n current = { label, left: col.left, count: 1 };\n } else {\n current.count++;\n }\n }\n if (current) months.push({ label: current.label, left: current.left, width: current.count * dw });\n return months;\n });\n\n protected readonly bars = computed((): BarLayout[] => {\n const tasks = this.normalizedTasks();\n const start = this.timelineStart().getTime();\n const dw = this.dayWidth();\n const rh = this.rowHeight();\n\n return tasks.map((t, i) => {\n const dayOffset = (t.start.getTime() - start) / 86400000;\n const duration = Math.max((t.end.getTime() - t.start.getTime()) / 86400000, 1);\n return {\n task: t,\n left: dayOffset * dw,\n width: duration * dw,\n top: i * rh,\n progress: Math.min(Math.max(t.progress, 0), 100),\n colorClass: t.color,\n };\n });\n });\n\n protected readonly depLines = computed((): DepLine[] => {\n const deps = this.dependencies();\n const barsMap = new Map<string, BarLayout>();\n for (const bar of this.bars()) barsMap.set(bar.task.id, bar);\n const rh = this.rowHeight();\n\n return deps\n .filter(d => barsMap.has(d.from) && barsMap.has(d.to))\n .map(d => {\n const from = barsMap.get(d.from)!;\n const to = barsMap.get(d.to)!;\n return {\n x1: from.left + from.width,\n y1: from.top + rh / 2,\n x2: to.left,\n y2: to.top + rh / 2,\n };\n });\n });\n\n protected readonly todayLeft = computed(() => {\n if (!this.showToday()) return -1;\n const start = this.timelineStart().getTime();\n const now = this.startOfDay(new Date()).getTime();\n const dayOffset = (now - start) / 86400000;\n if (dayOffset < 0 || dayOffset > this.totalDays()) return -1;\n return dayOffset * this.dayWidth();\n });\n\n protected readonly gridHeight = computed(() => this.normalizedTasks().length * this.rowHeight());\n\n protected onTaskClick(task: NormalizedTask): void {\n this.taskClick.emit(this.tasks().find(t => t.id === task.id)!);\n }\n\n protected onTaskKeydown(event: KeyboardEvent, task: NormalizedTask): void {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.onTaskClick(task);\n }\n }\n\n private startOfDay(d: Date): Date {\n return new Date(d.getFullYear(), d.getMonth(), d.getDate());\n }\n}\n","<div class=\"lc-gantt\">\n <div class=\"lc-gantt__container\">\n <!-- Left: Task labels -->\n <div class=\"lc-gantt__labels\" [style.width.px]=\"labelWidth()\">\n <div class=\"lc-gantt__labels-header\">{{ labelsHeader() }}</div>\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__label-row\" [style.height.px]=\"rowHeight()\">\n <span class=\"lc-gantt__label-text\" [title]=\"bar.task.title\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n\n <!-- Right: Timeline -->\n <div class=\"lc-gantt__timeline-wrapper\">\n <!-- Month headers -->\n <div class=\"lc-gantt__month-row\" [style.width.px]=\"timelineWidth()\">\n @for (mh of monthHeaders(); track mh.label + mh.left) {\n <div class=\"lc-gantt__month-cell\" [style.left.px]=\"mh.left\" [style.width.px]=\"mh.width\">\n {{ mh.label }}\n </div>\n }\n </div>\n\n <!-- Day headers -->\n <div class=\"lc-gantt__day-row\" [style.width.px]=\"timelineWidth()\">\n @for (col of dayColumns(); track col.left) {\n <div\n class=\"lc-gantt__day-cell\"\n [class.lc-gantt__day-cell--weekend]=\"col.isWeekend\"\n [style.left.px]=\"col.left\"\n [style.width.px]=\"dayWidth()\"\n >\n {{ col.label }}\n </div>\n }\n </div>\n\n <!-- Grid + Bars -->\n <div class=\"lc-gantt__grid\" [style.width.px]=\"timelineWidth()\" [style.height.px]=\"gridHeight()\">\n <!-- Weekend columns -->\n @for (col of dayColumns(); track col.left) {\n @if (col.isWeekend) {\n <div class=\"lc-gantt__weekend-bg\" [style.left.px]=\"col.left\" [style.width.px]=\"dayWidth()\" [style.height.px]=\"gridHeight()\"></div>\n }\n }\n\n <!-- Row borders -->\n @for (bar of bars(); track bar.task.id) {\n <div class=\"lc-gantt__row-line\" [style.top.px]=\"bar.top + rowHeight()\"></div>\n }\n\n <!-- Today marker -->\n @if (todayLeft() >= 0) {\n <div class=\"lc-gantt__today\" [style.left.px]=\"todayLeft()\" [style.height.px]=\"gridHeight()\"></div>\n }\n\n <!-- Dependency lines SVG -->\n @if (depLines().length) {\n <svg class=\"lc-gantt__dep-svg\" [attr.width]=\"timelineWidth()\" [attr.height]=\"gridHeight()\">\n <defs>\n <marker [attr.id]=\"markerId\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L8,3 L0,6 Z\" class=\"lc-gantt__arrow\" />\n </marker>\n </defs>\n @for (dep of depLines(); track $index) {\n <path\n [attr.d]=\"'M' + dep.x1 + ',' + dep.y1 + ' C' + (dep.x1 + 20) + ',' + dep.y1 + ' ' + (dep.x2 - 20) + ',' + dep.y2 + ' ' + dep.x2 + ',' + dep.y2\"\n class=\"lc-gantt__dep-line\"\n [attr.marker-end]=\"'url(#' + markerId + ')'\"\n />\n }\n </svg>\n }\n\n <!-- Task bars -->\n @for (bar of bars(); track bar.task.id) {\n <div\n [class]=\"'lc-gantt__bar lc-gantt__bar--' + bar.colorClass\"\n role=\"button\"\n tabindex=\"0\"\n [style.left.px]=\"bar.left\"\n [style.top.px]=\"bar.top + 8\"\n [style.width.px]=\"bar.width\"\n [style.height.px]=\"rowHeight() - 16\"\n [title]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n [attr.aria-label]=\"bar.task.title + ' (' + bar.progress + '%)'\"\n (click)=\"onTaskClick(bar.task)\"\n (keydown)=\"onTaskKeydown($event, bar.task)\"\n >\n @if (bar.progress > 0) {\n <div class=\"lc-gantt__bar-progress\" [style.width.%]=\"bar.progress\"></div>\n }\n <span class=\"lc-gantt__bar-label\">{{ bar.task.title }}</span>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n"],"mappings":";;AAyEA,IAAa,sBAAb,MAAa,oBAAmB;CACtB,OAAO,SAAS;CAML,WAAW,sBAAsB,EAAE,oBAAoB;CAG1E,QAAQ,MAAM,SAAuB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAGrC,eAAe,MAAyB,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAG3C,YAAY,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG7B,aAAa,MAAc,KAAI,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAG/B,WAAW,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAG5B,YAAY,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAGhC,SAAS,MAAc,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAG/B,eAAe,MAAc,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAGxC,YAAY,OAAM;CAED,kBAAkB,eACjC,KAAK,MAAK,CAAE,CAAC,KAAI,OAAM;EACrB,IAAI,EAAE;EACN,OAAO,EAAE;EACT,OAAO,EAAE,iBAAiB,OAAO,EAAE,QAAQ,IAAI,KAAK,EAAE,KAAK;EAC3D,KAAK,EAAE,eAAe,OAAO,EAAE,MAAM,IAAI,KAAK,EAAE,GAAG;EACnD,UAAU,EAAE,YAAY;EACxB,OAAO,EAAE,SAAS;EAClB,OAAO,EAAE,SAAS;CACnB,EAAC,GACH,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAEkB,gBAAgB,eAAc;EAC/C,MAAM,QAAQ,KAAK,gBAAe;EAClC,IAAI,CAAC,MAAM,QAAQ,uBAAO,IAAI,KAAI;EAClC,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM,KAAI,MAAK,EAAE,MAAM,QAAO,CAAE,CAAC;EACzD,MAAM,IAAI,IAAI,KAAK,GAAG;EACtB,EAAE,QAAQ,EAAE,QAAO,IAAK,CAAC;EACzB,OAAO,KAAK,WAAW,CAAC;CAC1B,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,cAAc,eAAc;EAC7C,MAAM,QAAQ,KAAK,gBAAe;EAClC,IAAI,CAAC,MAAM,QAAQ,uBAAO,IAAI,KAAI;EAClC,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM,KAAI,MAAK,EAAE,IAAI,QAAO,CAAE,CAAC;EACvD,MAAM,IAAI,IAAI,KAAK,GAAG;EACtB,EAAE,QAAQ,EAAE,QAAO,IAAK,CAAC;EACzB,OAAO,KAAK,WAAW,CAAC;CAC1B,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,YAAY,eAAc;EAC3C,MAAM,QAAQ,KAAK,cAAa;EAChC,MAAM,MAAM,KAAK,YAAW;EAC5B,OAAO,KAAK,MAAM,IAAI,QAAO,IAAK,MAAM,QAAO,KAAM,KAAQ,IAAI;CACnE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,gBAAgB,eAAe,KAAK,UAAS,IAAK,KAAK,SAAQ,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAElE,aAAa,eAAc;EAC5C,MAAM,QAAQ,KAAK,cAAa;EAChC,MAAM,OAAO,KAAK,UAAS;EAC3B,MAAM,KAAK,KAAK,SAAQ;EACxB,OAAO,MAAM,KAAK,EAAE,QAAQ,KAAI,IAAK,GAAG,MAAK;GAC3C,MAAM,IAAI,IAAI,KAAK,KAAK;GACxB,EAAE,QAAQ,EAAE,QAAO,IAAK,CAAC;GACzB,OAAO;IACL,MAAM;IACN,MAAM,IAAI;IACV,OAAO,EAAE,QAAO;IAChB,WAAW,EAAE,OAAM,MAAO,KAAK,EAAE,OAAM,MAAO;IAC9C,YAAY,EAAE,QAAO,MAAO,IAAI,EAAE,mBAAmB,KAAK,OAAM,GAAI,EAAE,OAAO,QAAO,CAAE,IAAI;GAC3F;EACH,CAAC;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,eAAe,eAAc;EAC9C,MAAM,OAAO,KAAK,WAAU;EAC5B,MAAM,KAAK,KAAK,SAAQ;EACxB,MAAM,SAAS,KAAK,OAAM;EAC1B,MAAM,SAA2D,CAAA;EACjE,IAAI,UAAiE;EAErE,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,QAAQ,IAAI,KAAK,mBAAmB,QAAQ;IAAE,OAAO;IAAS,MAAM;GAAS,CAAE;GACrF,IAAI,CAAC,WAAW,QAAQ,UAAU,OAAO;IACvC,IAAI,SAAS,OAAO,KAAK;KAAE,OAAO,QAAQ;KAAO,MAAM,QAAQ;KAAM,OAAO,QAAQ,QAAQ;IAAE,CAAE;IAChG,UAAU;KAAE;KAAO,MAAM,IAAI;KAAM,OAAO;IAAC;GAC7C,OACE,QAAQ;EAEZ;EACA,IAAI,SAAS,OAAO,KAAK;GAAE,OAAO,QAAQ;GAAO,MAAM,QAAQ;GAAM,OAAO,QAAQ,QAAQ;EAAE,CAAE;EAChG,OAAO;CACT,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,OAAO,eAA2B;EACnD,MAAM,QAAQ,KAAK,gBAAe;EAClC,MAAM,QAAQ,KAAK,cAAa,CAAE,CAAC,QAAO;EAC1C,MAAM,KAAK,KAAK,SAAQ;EACxB,MAAM,KAAK,KAAK,UAAS;EAEzB,OAAO,MAAM,KAAK,GAAG,MAAK;GACxB,MAAM,aAAa,EAAE,MAAM,QAAO,IAAK,SAAS;GAChD,MAAM,WAAW,KAAK,KAAK,EAAE,IAAI,QAAO,IAAK,EAAE,MAAM,QAAO,KAAM,OAAU,CAAC;GAC7E,OAAO;IACL,MAAM;IACN,MAAM,YAAY;IAClB,OAAO,WAAW;IAClB,KAAK,IAAI;IACT,UAAU,KAAK,IAAI,KAAK,IAAI,EAAE,UAAU,CAAC,GAAG,GAAG;IAC/C,YAAY,EAAE;GACf;EACH,CAAC;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,WAAW,eAAyB;EACrD,MAAM,OAAO,KAAK,aAAY;EAC9B,MAAM,0BAAU,IAAI,IAAG;EACvB,KAAK,MAAM,OAAO,KAAK,KAAI,GAAI,QAAQ,IAAI,IAAI,KAAK,IAAI,GAAG;EAC3D,MAAM,KAAK,KAAK,UAAS;EAEzB,OAAO,KACJ,QAAO,MAAK,QAAQ,IAAI,EAAE,IAAI,KAAK,QAAQ,IAAI,EAAE,EAAE,CAAC,CAAC,CACrD,KAAI,MAAI;GACP,MAAM,OAAO,QAAQ,IAAI,EAAE,IAAI;GAC/B,MAAM,KAAK,QAAQ,IAAI,EAAE,EAAE;GAC3B,OAAO;IACL,IAAI,KAAK,OAAO,KAAK;IACrB,IAAI,KAAK,MAAM,KAAK;IACpB,IAAI,GAAG;IACP,IAAI,GAAG,MAAM,KAAK;GACnB;EACH,CAAC;CACL,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,YAAY,eAAc;EAC3C,IAAI,CAAC,KAAK,UAAS,GAAI,OAAO;EAC9B,MAAM,QAAQ,KAAK,cAAa,CAAE,CAAC,QAAO;EAE1C,MAAM,aADM,KAAK,2BAAW,IAAI,KAAI,CAAE,CAAC,CAAC,QACrB,IAAM,SAAS;EAClC,IAAI,YAAY,KAAK,YAAY,KAAK,UAAS,GAAI,OAAO;EAC1D,OAAO,YAAY,KAAK,SAAQ;CAClC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,aAAa,eAAe,KAAK,gBAAe,CAAE,CAAC,SAAS,KAAK,UAAS,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEtF,YAAY,MAAoB;EACxC,KAAK,UAAU,KAAK,KAAK,MAAK,CAAE,CAAC,MAAK,MAAK,EAAE,OAAO,KAAK,EAAE,CAAE;CAC/D;CAEU,cAAc,OAAsB,MAAoB;EAChE,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;GAC9C,MAAM,eAAc;GACpB,KAAK,YAAY,IAAI;EACvB;CACF;CAEQ,WAAW,GAAO;EACxB,OAAO,IAAI,KAAK,EAAE,YAAW,GAAI,EAAE,SAAQ,GAAI,EAAE,QAAO,CAAE;CAC5D;CA/KW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAmB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAnB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAmB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,QAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA,EAAA,WAAA,YAAA;EAAA,UAAA;EAAA,UCzEhC;EAmGA,QAAA,CAAA,+5GAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AD1Ba,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAmB,YAAA,CAAA;EAxB/B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAgB,YACd;GAAI,iBAGC,wBAAwB;GAAM,UAAA;GAAA,QAAA,CAAA,+5GAAA;EAAA,CAAA"}
|