@lifecorner/angular-ui-kit 3.19.0 → 3.19.1
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-combobox.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-combobox.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-drawer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-drawer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-field-group.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-field-group.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.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-log-viewer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-log-viewer.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-toast.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-toast.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/theme-contrast.spec.ts +78 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-drawer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.ts","../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n untracked,\n inject,\n HostListener,\n OnDestroy,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type DrawerPosition = 'left' | 'right';\nexport type DrawerSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Duration of the slide-out transition (see drawer.component.scss). */\nconst CLOSE_ANIMATION_MS = 250;\n\n/**\n * Drawer component for slide-out overlay panels.\n *\n * Features:\n * - Slide-in from left or right\n * - Size presets (sm, md, lg, xl)\n * - Optional backdrop overlay with click-to-close\n * - Close on Escape key support\n * - Heading text display\n * - Content projection for arbitrary body content\n * - Accessible with ARIA dialog role, focus trap and focus restore\n *\n * @example\n * ```html\n * <lc-drawer [open]=\"showPanel\" position=\"right\" size=\"md\"\n * heading=\"Settings\" (closed)=\"showPanel = false\">\n * <p>Any content goes here</p>\n * </lc-drawer>\n * ```\n */\n@Component({\n selector: 'lc-drawer',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './drawer.component.html',\n styleUrl: './drawer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DrawerComponent implements OnDestroy {\n private static nextId = 0;\n\n /** Whether the drawer is open. */\n readonly open = input<boolean>(false);\n\n /** Slide-in direction. */\n readonly position = input<DrawerPosition>('right');\n\n /** Predefined width (sm=320, md=400, lg=520, xl=640). */\n readonly size = input<DrawerSize>('md');\n\n /** Optional heading shown in the drawer header. */\n readonly heading = input<string>('');\n\n /** Whether the overlay backdrop is shown. */\n readonly hasOverlay = input<boolean>(true);\n\n /** Whether clicking the overlay closes the drawer. */\n readonly closeOnOverlayClick = input<boolean>(true);\n\n /** Whether pressing Escape closes the drawer. */\n readonly closeOnEscape = input<boolean>(true);\n\n /** ARIA label for assistive technology. */\n readonly ariaLabel = input<string>('');\n\n /** Accessible name of the close button. */\n readonly closeAriaLabel = input('Close drawer');\n\n /** Emitted when the drawer should close. */\n readonly closed = output<void>();\n\n /** Internal visibility — stays true during the slide-out animation. */\n protected _visible = signal(false);\n\n /** Width token → CSS. */\n protected readonly widthValue = computed(() => {\n const map: Record<DrawerSize, string> = {\n sm: '320px',\n md: '400px',\n lg: '520px',\n xl: '640px',\n };\n return map[this.size()];\n });\n\n protected readonly panelClasses = computed(() => {\n const cls = ['lc-drawer__panel', `lc-drawer__panel--${this.position()}`];\n if (this.open()) cls.push('lc-drawer__panel--open');\n return cls.join(' ');\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly drawerId = `lc-drawer-${++DrawerComponent.nextId}`;\n private closeTimer?: ReturnType<typeof setTimeout>;\n private originalOverflow?: string;\n\n constructor() {\n effect(() => {\n const isOpen = this.open();\n untracked(() => {\n if (isOpen) {\n // Re-opening within the close animation must cancel the pending hide,\n // otherwise the stale timer hides the drawer and unlocks scroll while\n // `open()` is still true.\n this.clearCloseTimer();\n this._visible.set(true);\n this.lockScroll();\n this.overlayStack.push(this.drawerId);\n } else if (this._visible()) {\n this.overlayStack.remove(this.drawerId);\n // Keep the panel mounted until the slide-out transition has finished\n this.closeTimer = setTimeout(() => {\n this.closeTimer = undefined;\n this._visible.set(false);\n this.unlockScroll();\n }, CLOSE_ANIMATION_MS);\n }\n });\n });\n }\n\n ngOnDestroy(): void {\n // Navigating away while open must not leave the page scroll-locked\n this.clearCloseTimer();\n this.overlayStack.remove(this.drawerId);\n this.unlockScroll();\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (menu, popover, …)\n * sits above this drawer — the click belongs to that layer.\n */\n protected onOverlayClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n if (this.closeOnOverlayClick()) {\n this.close();\n }\n }\n\n /** Handle close action. */\n protected close(): void {\n this.closed.emit();\n }\n\n /**\n * Escape closes the drawer only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this.open()) return;\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.close();\n }\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow !== undefined) return;\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow === undefined) return;\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n private clearCloseTimer(): void {\n if (this.closeTimer !== undefined) {\n clearTimeout(this.closeTimer);\n this.closeTimer = undefined;\n }\n }\n}\n","@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-drawer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.ts","../../../../libs/angular-ui-kit/src/lib/drawer/drawer.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n untracked,\n inject,\n HostListener,\n OnDestroy,\n} from '@angular/core';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\nimport { OverlayStackService } from '@lifecorner/angular-ui-kit/internal/overlay';\n\nexport type DrawerPosition = 'left' | 'right';\nexport type DrawerSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/** Duration of the slide-out transition (see drawer.component.scss). */\nconst CLOSE_ANIMATION_MS = 250;\n\n/**\n * Drawer component for slide-out overlay panels.\n *\n * Features:\n * - Slide-in from left or right\n * - Size presets (sm, md, lg, xl)\n * - Optional backdrop overlay with click-to-close\n * - Close on Escape key support\n * - Heading text display\n * - Content projection for arbitrary body content\n * - Accessible with ARIA dialog role, focus trap and focus restore\n *\n * @example\n * ```html\n * <lc-drawer [open]=\"showPanel\" position=\"right\" size=\"md\"\n * heading=\"Settings\" (closed)=\"showPanel = false\">\n * <p>Any content goes here</p>\n * </lc-drawer>\n * ```\n */\n@Component({\n selector: 'lc-drawer',\n standalone: true,\n imports: [A11yModule, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_X_MARK])],\n templateUrl: './drawer.component.html',\n styleUrl: './drawer.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DrawerComponent implements OnDestroy {\n private static nextId = 0;\n\n /** Whether the drawer is open. */\n readonly open = input<boolean>(false);\n\n /** Slide-in direction. */\n readonly position = input<DrawerPosition>('right');\n\n /** Predefined width (sm=320, md=400, lg=520, xl=640). */\n readonly size = input<DrawerSize>('md');\n\n /** Optional heading shown in the drawer header. */\n readonly heading = input<string>('');\n\n /** Whether the overlay backdrop is shown. */\n readonly hasOverlay = input<boolean>(true);\n\n /** Whether clicking the overlay closes the drawer. */\n readonly closeOnOverlayClick = input<boolean>(true);\n\n /** Whether pressing Escape closes the drawer. */\n readonly closeOnEscape = input<boolean>(true);\n\n /** ARIA label for assistive technology. */\n readonly ariaLabel = input<string>('');\n\n /** Accessible name of the close button. */\n readonly closeAriaLabel = input('Close drawer');\n\n /** Emitted when the drawer should close. */\n readonly closed = output<void>();\n\n /** Internal visibility — stays true during the slide-out animation. */\n protected _visible = signal(false);\n\n /** Width token → CSS. */\n protected readonly widthValue = computed(() => {\n const map: Record<DrawerSize, string> = {\n sm: '320px',\n md: '400px',\n lg: '520px',\n xl: '640px',\n };\n return map[this.size()];\n });\n\n protected readonly panelClasses = computed(() => {\n const cls = ['lc-drawer__panel', `lc-drawer__panel--${this.position()}`];\n if (this.open()) cls.push('lc-drawer__panel--open');\n return cls.join(' ');\n });\n\n private readonly overlayStack = inject(OverlayStackService);\n /** Identifies this instance in the overlay stack. */\n private readonly drawerId = `lc-drawer-${++DrawerComponent.nextId}`;\n private closeTimer?: ReturnType<typeof setTimeout>;\n private originalOverflow?: string;\n\n constructor() {\n effect(() => {\n const isOpen = this.open();\n untracked(() => {\n if (isOpen) {\n // Re-opening within the close animation must cancel the pending hide,\n // otherwise the stale timer hides the drawer and unlocks scroll while\n // `open()` is still true.\n this.clearCloseTimer();\n this._visible.set(true);\n this.lockScroll();\n this.overlayStack.push(this.drawerId);\n } else if (this._visible()) {\n this.overlayStack.remove(this.drawerId);\n // Keep the panel mounted until the slide-out transition has finished\n this.closeTimer = setTimeout(() => {\n this.closeTimer = undefined;\n this._visible.set(false);\n this.unlockScroll();\n }, CLOSE_ANIMATION_MS);\n }\n });\n });\n }\n\n ngOnDestroy(): void {\n // Navigating away while open must not leave the page scroll-locked\n this.clearCloseTimer();\n this.overlayStack.remove(this.drawerId);\n this.unlockScroll();\n }\n\n /**\n * Handle backdrop click. Ignored while another overlay (menu, popover, …)\n * sits above this drawer — the click belongs to that layer.\n */\n protected onOverlayClick(event: MouseEvent): void {\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n if (this.closeOnOverlayClick()) {\n this.close();\n }\n }\n\n /** Handle close action. */\n protected close(): void {\n this.closed.emit();\n }\n\n /**\n * Escape closes the drawer only while it is the top-most overlay; the event\n * is consumed either way so overlays underneath stay open.\n */\n @HostListener('document:keydown', ['$event'])\n protected onKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape' || !this.open()) return;\n if (!this.overlayStack.claim(this.drawerId, event)) return;\n event.stopPropagation();\n if (this.closeOnEscape()) {\n this.close();\n }\n }\n\n private lockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow !== undefined) return;\n this.originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n private unlockScroll(): void {\n if (typeof document === 'undefined' || this.originalOverflow === undefined) return;\n document.body.style.overflow = this.originalOverflow;\n this.originalOverflow = undefined;\n }\n\n private clearCloseTimer(): void {\n if (this.closeTimer !== undefined) {\n clearTimeout(this.closeTimer);\n this.closeTimer = undefined;\n }\n }\n}\n","@if (_visible()) {\n <div class=\"lc-drawer\">\n <!-- Overlay -->\n @if (hasOverlay()) {\n <div\n class=\"lc-drawer__overlay\"\n [class.lc-drawer__overlay--visible]=\"open()\"\n (click)=\"onOverlayClick($event)\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel. Focus moves in on open and back to the opener when the panel\n is torn down (cdkTrapFocusAutoCapture); focus is only trapped while\n the backdrop makes the page behind inert. -->\n <aside\n [class]=\"panelClasses()\"\n [style.width]=\"widthValue()\"\n role=\"dialog\"\n [attr.aria-label]=\"ariaLabel() || heading() || 'Drawer'\"\n [attr.aria-modal]=\"hasOverlay() ? 'true' : null\"\n [cdkTrapFocus]=\"hasOverlay()\"\n [cdkTrapFocusAutoCapture]=\"true\"\n >\n <!-- Header -->\n @if (heading()) {\n <header class=\"lc-drawer__header\">\n <h2 class=\"lc-drawer__heading\">{{ heading() }}</h2>\n <button\n type=\"button\"\n class=\"lc-drawer__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n </header>\n } @else {\n <button\n type=\"button\"\n class=\"lc-drawer__close lc-drawer__close--floating\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" size=\"sm\" [decorative]=\"true\" />\n </button>\n }\n\n <!-- Projected content -->\n <div class=\"lc-drawer__body\">\n <ng-content />\n </div>\n </aside>\n </div>\n}\n"],"mappings":";;;;;;;AAsBA,MAAM,qBAAqB;AA+B3B,IAAa,kBAAb,MAAa,gBAAe;CAClB,OAAO,SAAS;CAGf,OAAO,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAG5B,WAAW,MAAsB,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGzC,OAAO,MAAkB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAG9B,UAAU,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAG3B,aAAa,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,sBAAsB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,sBAAA,CAAA,+BAAA,CAAA,CAAA;CAG1C,gBAAgB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAGpC,YAAY,MAAc,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG7B,iBAAiB,MAAM,gBAAe,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAGtC,SAAS,OAAM;CAGd,WAAW,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGf,aAAa,eAAc;EAO5C,OAAO;GALL,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EAEC,EAAI,KAAK,KAAI;CACtB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,eAAe,eAAc;EAC9C,MAAM,MAAM,CAAC,oBAAoB,qBAAqB,KAAK,SAAQ,GAAI;EACvE,IAAI,KAAK,KAAI,GAAI,IAAI,KAAK,wBAAwB;EAClD,OAAO,IAAI,KAAK,GAAG;CACrB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEe,eAAe,OAAO,mBAAmB;CAEzC,WAAW,aAAa,EAAE,gBAAgB;CACnD;CACA;CAER,cAAA;EACE,aAAY;GACV,MAAM,SAAS,KAAK,KAAI;GACxB,gBAAe;IACb,IAAI,QAAQ;KAIV,KAAK,gBAAe;KACpB,KAAK,SAAS,IAAI,IAAI;KACtB,KAAK,WAAU;KACf,KAAK,aAAa,KAAK,KAAK,QAAQ;IACtC,OAAO,IAAI,KAAK,SAAQ,GAAI;KAC1B,KAAK,aAAa,OAAO,KAAK,QAAQ;KAEtC,KAAK,aAAa,iBAAgB;MAChC,KAAK,aAAa;MAClB,KAAK,SAAS,IAAI,KAAK;MACvB,KAAK,aAAY;KACnB,GAAG,kBAAkB;IACvB;GACF,CAAC;EACH,CAAC;CACH;CAEA,cAAW;EAET,KAAK,gBAAe;EACpB,KAAK,aAAa,OAAO,KAAK,QAAQ;EACtC,KAAK,aAAY;CACnB;CAMU,eAAe,OAAiB;EACxC,IAAI,CAAC,KAAK,aAAa,MAAM,KAAK,UAAU,KAAK,GAAG;EACpD,IAAI,KAAK,oBAAmB,GAC1B,KAAK,MAAK;CAEd;CAGU,QAAK;EACb,KAAK,OAAO,KAAI;CAClB;CAOU,UAAU,OAAoB;EACtC,IAAI,MAAM,QAAQ,YAAY,CAAC,KAAK,KAAI,GAAI;EAC5C,IAAI,CAAC,KAAK,aAAa,MAAM,KAAK,UAAU,KAAK,GAAG;EACpD,MAAM,gBAAe;EACrB,IAAI,KAAK,cAAa,GACpB,KAAK,MAAK;CAEd;CAEQ,aAAU;EAChB,IAAI,OAAO,aAAa,eAAe,KAAK,qBAAqB,QAAW;EAC5E,KAAK,mBAAmB,SAAS,KAAK,MAAM;EAC5C,SAAS,KAAK,MAAM,WAAW;CACjC;CAEQ,eAAY;EAClB,IAAI,OAAO,aAAa,eAAe,KAAK,qBAAqB,QAAW;EAC5E,SAAS,KAAK,MAAM,WAAW,KAAK;EACpC,KAAK,mBAAmB;CAC1B;CAEQ,kBAAe;EACrB,IAAI,KAAK,eAAe,QAAW;GACjC,aAAa,KAAK,UAAU;GAC5B,KAAK,aAAa;EACpB;CACF;CA1IW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAe,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAf,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAe,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,MAAA;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,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;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,qBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;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,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA,EAAA,QAAA,SAAA;EAAA,MAAA,EAAA,WAAA,EAAA,oBAAA,oBAAA,EAAA;EAAA,UAAA;EAAA,UCrD5B;EAuDA,QAAA,CAAA,ogEAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MDRY;GAAU;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,gBAAA,yBAAA;IAAA,UAAA,CAAA,cAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAa,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;EAAA,eACpB,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;EAAC,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAKrC,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAe,YAAA,CAAA;EAT3B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAW,YACT;GAAI,SACP,CAAC,YAAY,aAAa;GAAC,eACrB,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;GAAC,iBAG/B,wBAAwB;GAAM,UAAA;GAAA,QAAA,CAAA,ogEAAA;EAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiH9C,MAAA;GAAa,MAAA,CAAA,oBAAoB,CAAC,QAAQ,CAAC"}
|
|
@@ -66,7 +66,7 @@ var FieldGroupComponent = class FieldGroupComponent {
|
|
|
66
66
|
classAttribute: "lc-field-group"
|
|
67
67
|
},
|
|
68
68
|
ngImport: i0,
|
|
69
|
-
template: "@if (icon()) {\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize()\" class=\"lc-field-group__icon\"></lc-icon>\n}\n\n<div class=\"lc-field-group__content\">\n <span class=\"lc-field-group__label\">{{ label() }}</span>\n @if (value()) {\n <p class=\"lc-field-group__value\">{{ value() }}</p>\n } @else {\n <div class=\"lc-field-group__value\">\n <ng-content></ng-content>\n </div>\n }\n</div>\n",
|
|
69
|
+
template: "@if (icon()) {\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize()\" class=\"lc-field-group__icon\" [decorative]=\"true\"></lc-icon>\n}\n\n<div class=\"lc-field-group__content\">\n <span class=\"lc-field-group__label\">{{ label() }}</span>\n @if (value()) {\n <p class=\"lc-field-group__value\">{{ value() }}</p>\n } @else {\n <div class=\"lc-field-group__value\">\n <ng-content></ng-content>\n </div>\n }\n</div>\n",
|
|
70
70
|
styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:.5rem;padding:1rem;border-radius:var(--border-radius-md);background:var(--color-surface);transition:background-color .2s ease}:host:hover{background:var(--color-surface-hover)}:host.lc-field-group--with-icon{flex-direction:row;align-items:flex-start;gap:1rem;padding:1.25rem}:host.lc-field-group--compact{padding:.75rem;gap:.25rem}:host.lc-field-group--compact.lc-field-group--with-icon{padding:.75rem 1rem;gap:.75rem}.lc-field-group__icon{color:var(--color-primary);flex-shrink:0;margin-top:.125rem}.lc-field-group__content{display:flex;flex-direction:column;gap:.25rem;flex:1;min-width:0}.lc-field-group__label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-secondary);line-height:1.2}.lc-field-group__value{font-size:1rem;font-weight:500;color:var(--color-text-primary);margin:0;line-height:1.4;word-break:break-word}.lc-field-group__value:empty:after{content:\"\\2014\";color:var(--color-text-tertiary)}\n"],
|
|
71
71
|
dependencies: [{
|
|
72
72
|
kind: "component",
|
|
@@ -102,7 +102,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
102
102
|
"[class.lc-field-group--with-icon]": "icon()",
|
|
103
103
|
"[class.lc-field-group--compact]": "compact()"
|
|
104
104
|
},
|
|
105
|
-
template: "@if (icon()) {\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize()\" class=\"lc-field-group__icon\"></lc-icon>\n}\n\n<div class=\"lc-field-group__content\">\n <span class=\"lc-field-group__label\">{{ label() }}</span>\n @if (value()) {\n <p class=\"lc-field-group__value\">{{ value() }}</p>\n } @else {\n <div class=\"lc-field-group__value\">\n <ng-content></ng-content>\n </div>\n }\n</div>\n",
|
|
105
|
+
template: "@if (icon()) {\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize()\" class=\"lc-field-group__icon\" [decorative]=\"true\"></lc-icon>\n}\n\n<div class=\"lc-field-group__content\">\n <span class=\"lc-field-group__label\">{{ label() }}</span>\n @if (value()) {\n <p class=\"lc-field-group__value\">{{ value() }}</p>\n } @else {\n <div class=\"lc-field-group__value\">\n <ng-content></ng-content>\n </div>\n }\n</div>\n",
|
|
106
106
|
styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:.5rem;padding:1rem;border-radius:var(--border-radius-md);background:var(--color-surface);transition:background-color .2s ease}:host:hover{background:var(--color-surface-hover)}:host.lc-field-group--with-icon{flex-direction:row;align-items:flex-start;gap:1rem;padding:1.25rem}:host.lc-field-group--compact{padding:.75rem;gap:.25rem}:host.lc-field-group--compact.lc-field-group--with-icon{padding:.75rem 1rem;gap:.75rem}.lc-field-group__icon{color:var(--color-primary);flex-shrink:0;margin-top:.125rem}.lc-field-group__content{display:flex;flex-direction:column;gap:.25rem;flex:1;min-width:0}.lc-field-group__label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-secondary);line-height:1.2}.lc-field-group__value{font-size:1rem;font-weight:500;color:var(--color-text-primary);margin:0;line-height:1.4;word-break:break-word}.lc-field-group__value:empty:after{content:\"\\2014\";color:var(--color-text-tertiary)}\n"]
|
|
107
107
|
}]
|
|
108
108
|
}],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-field-group.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/field-group/field-group.component.ts","../../../../libs/angular-ui-kit/src/lib/field-group/field-group.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { IconComponent } from '@lifecorner/angular-ui-kit/icon';\n\n/**\n * Field group component for displaying label-value pairs.\n *\n * Features:\n * - Consistent label-value layout for read-only data\n * - Optional leading icon from Tabler Icons\n * - Compact mode for dense layouts\n * - Content projection for custom value rendering\n * - Configurable icon size (xs, sm, md)\n *\n * @example\n * ```html\n * <lc-field-group label=\"Email\" value=\"user@example.com\" />\n * <lc-field-group label=\"Language\" value=\"English\" icon=\"globe-alt\" />\n * ```\n */\n@Component({\n selector: 'lc-field-group',\n standalone: true,\n imports: [IconComponent],\n templateUrl: './field-group.component.html',\n styleUrls: ['./field-group.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'lc-field-group',\n '[class.lc-field-group--with-icon]': 'icon()',\n '[class.lc-field-group--compact]': 'compact()',\n },\n})\nexport class FieldGroupComponent {\n /**\n * Label text displayed above the value\n */\n label = input.required<string>();\n\n /**\n * Value text to display (optional if using content projection)\n */\n value = input<string>();\n\n /**\n * Optional icon name from Tabler Icons\n */\n icon = input<string>();\n\n /**\n * Icon size\n * @default 'sm'\n */\n iconSize = input<'xs' | 'sm' | 'md'>('sm');\n\n /**\n * Use compact styling with less padding\n * @default false\n */\n compact = input<boolean>(false);\n}\n","@if (icon()) {\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize()\" class=\"lc-field-group__icon\"></lc-icon>\n}\n\n<div class=\"lc-field-group__content\">\n <span class=\"lc-field-group__label\">{{ label() }}</span>\n @if (value()) {\n <p class=\"lc-field-group__value\">{{ value() }}</p>\n } @else {\n <div class=\"lc-field-group__value\">\n <ng-content></ng-content>\n </div>\n }\n</div>\n"],"mappings":";;;AAgCA,IAAa,sBAAb,MAAa,oBAAmB;CAI9B,QAAQ,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAKhC,QAAQ,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAKvB,OAAO,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAMtB,WAAW,MAA0B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAM1C,UAAU,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CA1BpB,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,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;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,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,MAAA;GAAA,YAAA;IAAA,mCAAA;IAAA,iCAAA;GAAA;GAAA,gBAAA;EAAA;EAAA,UAAA;EAAA,UChChC;EAcA,QAAA,CAAA,sgCAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MDQY;GAAa,UAAA;GAAA,QAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;GAAA;EAAA,CAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAUZ,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAmB,YAAA,CAAA;EAb/B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAgB,YACd;GAAI,SACP,CAAC,aAAa;GAAC,iBAGP,wBAAwB;GAAM,MACzC;IACJ,OAAO;IACP,qCAAqC;IACrC,mCAAmC;GACpC;GAAA,UAAA;GAAA,QAAA,CAAA,sgCAAA;EAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-field-group.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/field-group/field-group.component.ts","../../../../libs/angular-ui-kit/src/lib/field-group/field-group.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { IconComponent } from '@lifecorner/angular-ui-kit/icon';\n\n/**\n * Field group component for displaying label-value pairs.\n *\n * Features:\n * - Consistent label-value layout for read-only data\n * - Optional leading icon from Tabler Icons\n * - Compact mode for dense layouts\n * - Content projection for custom value rendering\n * - Configurable icon size (xs, sm, md)\n *\n * @example\n * ```html\n * <lc-field-group label=\"Email\" value=\"user@example.com\" />\n * <lc-field-group label=\"Language\" value=\"English\" icon=\"globe-alt\" />\n * ```\n */\n@Component({\n selector: 'lc-field-group',\n standalone: true,\n imports: [IconComponent],\n templateUrl: './field-group.component.html',\n styleUrls: ['./field-group.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'lc-field-group',\n '[class.lc-field-group--with-icon]': 'icon()',\n '[class.lc-field-group--compact]': 'compact()',\n },\n})\nexport class FieldGroupComponent {\n /**\n * Label text displayed above the value\n */\n label = input.required<string>();\n\n /**\n * Value text to display (optional if using content projection)\n */\n value = input<string>();\n\n /**\n * Optional icon name from Tabler Icons\n */\n icon = input<string>();\n\n /**\n * Icon size\n * @default 'sm'\n */\n iconSize = input<'xs' | 'sm' | 'md'>('sm');\n\n /**\n * Use compact styling with less padding\n * @default false\n */\n compact = input<boolean>(false);\n}\n","@if (icon()) {\n <lc-icon [name]=\"icon()!\" [size]=\"iconSize()\" class=\"lc-field-group__icon\" [decorative]=\"true\"></lc-icon>\n}\n\n<div class=\"lc-field-group__content\">\n <span class=\"lc-field-group__label\">{{ label() }}</span>\n @if (value()) {\n <p class=\"lc-field-group__value\">{{ value() }}</p>\n } @else {\n <div class=\"lc-field-group__value\">\n <ng-content></ng-content>\n </div>\n }\n</div>\n"],"mappings":";;;AAgCA,IAAa,sBAAb,MAAa,oBAAmB;CAI9B,QAAQ,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAKhC,QAAQ,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAKvB,OAAO,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAMtB,WAAW,MAA0B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAM1C,UAAU,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CA1BpB,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,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;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,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,MAAA;GAAA,YAAA;IAAA,mCAAA;IAAA,iCAAA;GAAA;GAAA,gBAAA;EAAA;EAAA,UAAA;EAAA,UChChC;EAcA,QAAA,CAAA,sgCAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MDQY;GAAa,UAAA;GAAA,QAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;GAAA;EAAA,CAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAUZ,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAmB,YAAA,CAAA;EAb/B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAgB,YACd;GAAI,SACP,CAAC,aAAa;GAAC,iBAGP,wBAAwB;GAAM,MACzC;IACJ,OAAO;IACP,qCAAqC;IACrC,mCAAmC;GACpC;GAAA,UAAA;GAAA,QAAA,CAAA,sgCAAA;EAAA,CAAA"}
|
|
@@ -168,7 +168,7 @@ var FileUploadComponent = class FileUploadComponent {
|
|
|
168
168
|
isSignal: true
|
|
169
169
|
}],
|
|
170
170
|
ngImport: i0,
|
|
171
|
-
template: "<div\n [class]=\"dropzoneClasses()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (click)=\"openFilePicker()\"\n role=\"button\"\n tabindex=\"0\"\n (keydown.enter)=\"openFilePicker()\"\n (keydown.space)=\"openFilePicker()\"\n [attr.aria-label]=\"dropzoneAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n <input\n #fileInput\n type=\"file\"\n class=\"file-upload__input\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileChange($event)\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n <div class=\"file-upload__content\">\n <lc-icon name=\"cloud-arrow-up\" class=\"file-upload__icon\"></lc-icon>\n <!-- Real spaces between the lines, so the visible text reads as words\n that the drop zone's name contains (WCAG 2.5.3). -->\n <p class=\"file-upload__hint\">{{ hint() }}</p>\n @if (accept()) {\n &ngsp;<p class=\"file-upload__accept\">{{ acceptedLabel() }} {{ accept() }}</p>\n }\n @if (maxSizeBytes() > 0) {\n &ngsp;<p class=\"file-upload__accept\">{{ maxSizeLabel() }} {{ formatSize(maxSizeBytes()) }}</p>\n }\n </div>\n</div>\n\n@if (selectedFiles().length > 0) {\n <ul class=\"file-upload__list\">\n @for (f of selectedFiles(); track f.name; let i = $index) {\n <li class=\"file-upload__file\">\n <lc-icon name=\"document\" class=\"file-upload__file-icon\"></lc-icon>\n <span class=\"file-upload__file-name\">{{ f.name }}</span>\n <span class=\"file-upload__file-size\">{{ formatSize(f.size) }}</span>\n <button\n type=\"button\"\n class=\"file-upload__file-remove\"\n (click)=\"removeFile(i); $event.stopPropagation()\"\n [attr.aria-label]=\"removeFileAriaLabel()\"\n >\n <lc-icon name=\"x-mark\"></lc-icon>\n </button>\n </li>\n }\n </ul>\n}\n",
|
|
171
|
+
template: "<div\n [class]=\"dropzoneClasses()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (click)=\"openFilePicker()\"\n role=\"button\"\n tabindex=\"0\"\n (keydown.enter)=\"openFilePicker()\"\n (keydown.space)=\"openFilePicker()\"\n [attr.aria-label]=\"dropzoneAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n <input\n #fileInput\n type=\"file\"\n class=\"file-upload__input\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileChange($event)\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n <div class=\"file-upload__content\">\n <lc-icon name=\"cloud-arrow-up\" class=\"file-upload__icon\" [decorative]=\"true\"></lc-icon>\n <!-- Real spaces between the lines, so the visible text reads as words\n that the drop zone's name contains (WCAG 2.5.3). -->\n <p class=\"file-upload__hint\">{{ hint() }}</p>\n @if (accept()) {\n &ngsp;<p class=\"file-upload__accept\">{{ acceptedLabel() }} {{ accept() }}</p>\n }\n @if (maxSizeBytes() > 0) {\n &ngsp;<p class=\"file-upload__accept\">{{ maxSizeLabel() }} {{ formatSize(maxSizeBytes()) }}</p>\n }\n </div>\n</div>\n\n@if (selectedFiles().length > 0) {\n <ul class=\"file-upload__list\">\n @for (f of selectedFiles(); track f.name; let i = $index) {\n <li class=\"file-upload__file\">\n <lc-icon name=\"document\" class=\"file-upload__file-icon\" [decorative]=\"true\"></lc-icon>\n <span class=\"file-upload__file-name\">{{ f.name }}</span>\n <span class=\"file-upload__file-size\">{{ formatSize(f.size) }}</span>\n <button\n type=\"button\"\n class=\"file-upload__file-remove\"\n (click)=\"removeFile(i); $event.stopPropagation()\"\n [attr.aria-label]=\"removeFileAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" [decorative]=\"true\"></lc-icon>\n </button>\n </li>\n }\n </ul>\n}\n",
|
|
172
172
|
styles: [":host{display:block;width:100%}.file-upload{display:flex;align-items:center;justify-content:center;padding:2rem 1.5rem;border:2px dashed var(--color-border-strong);border-radius:var(--border-radius-lg);background-color:var(--color-surface-2);cursor:pointer;transition:border-color .15s ease,background-color .15s ease;text-align:center}.file-upload:hover{border-color:var(--color-primary-500);background-color:var(--color-surface-hover)}.file-upload:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.file-upload--drag-over{border-color:var(--color-primary-500);background-color:var(--color-surface-selected)}.file-upload--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.file-upload__input{position:absolute;width:0;height:0;overflow:hidden;opacity:0}.file-upload__content{display:flex;flex-direction:column;align-items:center;gap:.5rem}.file-upload__icon{--icon-size: 2rem;color:var(--color-primary-500)}.file-upload__hint{margin:0;font-size:.875rem;color:var(--color-text-secondary)}.file-upload__accept{margin:0;font-size:.75rem;color:var(--color-text-tertiary)}.file-upload__list{list-style:none;margin:.75rem 0 0;padding:0;display:flex;flex-direction:column;gap:.5rem}.file-upload__file{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--border-radius-md);font-size:.8125rem}.file-upload__file-icon{--icon-size: 1rem;color:var(--color-text-tertiary);flex-shrink:0}.file-upload__file-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--color-text-primary)}.file-upload__file-size{flex-shrink:0;color:var(--color-text-tertiary);font-size:.75rem}.file-upload__file-remove{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:none;border-radius:50%;background:var(--color-surface-hover);cursor:pointer;padding:0;--icon-size: .75rem;color:var(--color-text-secondary)}.file-upload__file-remove:hover{background:#9d0e0e2e;color:var(--color-error)}\n"],
|
|
173
173
|
dependencies: [{
|
|
174
174
|
kind: "component",
|
|
@@ -201,7 +201,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
201
201
|
imports: [IconComponent],
|
|
202
202
|
viewProviders: [ɵprovideKitIcons([ICON_FILE, ICON_X_MARK])],
|
|
203
203
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
204
|
-
template: "<div\n [class]=\"dropzoneClasses()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (click)=\"openFilePicker()\"\n role=\"button\"\n tabindex=\"0\"\n (keydown.enter)=\"openFilePicker()\"\n (keydown.space)=\"openFilePicker()\"\n [attr.aria-label]=\"dropzoneAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n <input\n #fileInput\n type=\"file\"\n class=\"file-upload__input\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileChange($event)\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n <div class=\"file-upload__content\">\n <lc-icon name=\"cloud-arrow-up\" class=\"file-upload__icon\"></lc-icon>\n <!-- Real spaces between the lines, so the visible text reads as words\n that the drop zone's name contains (WCAG 2.5.3). -->\n <p class=\"file-upload__hint\">{{ hint() }}</p>\n @if (accept()) {\n &ngsp;<p class=\"file-upload__accept\">{{ acceptedLabel() }} {{ accept() }}</p>\n }\n @if (maxSizeBytes() > 0) {\n &ngsp;<p class=\"file-upload__accept\">{{ maxSizeLabel() }} {{ formatSize(maxSizeBytes()) }}</p>\n }\n </div>\n</div>\n\n@if (selectedFiles().length > 0) {\n <ul class=\"file-upload__list\">\n @for (f of selectedFiles(); track f.name; let i = $index) {\n <li class=\"file-upload__file\">\n <lc-icon name=\"document\" class=\"file-upload__file-icon\"></lc-icon>\n <span class=\"file-upload__file-name\">{{ f.name }}</span>\n <span class=\"file-upload__file-size\">{{ formatSize(f.size) }}</span>\n <button\n type=\"button\"\n class=\"file-upload__file-remove\"\n (click)=\"removeFile(i); $event.stopPropagation()\"\n [attr.aria-label]=\"removeFileAriaLabel()\"\n >\n <lc-icon name=\"x-mark\"></lc-icon>\n </button>\n </li>\n }\n </ul>\n}\n",
|
|
204
|
+
template: "<div\n [class]=\"dropzoneClasses()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (click)=\"openFilePicker()\"\n role=\"button\"\n tabindex=\"0\"\n (keydown.enter)=\"openFilePicker()\"\n (keydown.space)=\"openFilePicker()\"\n [attr.aria-label]=\"dropzoneAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n <input\n #fileInput\n type=\"file\"\n class=\"file-upload__input\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileChange($event)\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n <div class=\"file-upload__content\">\n <lc-icon name=\"cloud-arrow-up\" class=\"file-upload__icon\" [decorative]=\"true\"></lc-icon>\n <!-- Real spaces between the lines, so the visible text reads as words\n that the drop zone's name contains (WCAG 2.5.3). -->\n <p class=\"file-upload__hint\">{{ hint() }}</p>\n @if (accept()) {\n &ngsp;<p class=\"file-upload__accept\">{{ acceptedLabel() }} {{ accept() }}</p>\n }\n @if (maxSizeBytes() > 0) {\n &ngsp;<p class=\"file-upload__accept\">{{ maxSizeLabel() }} {{ formatSize(maxSizeBytes()) }}</p>\n }\n </div>\n</div>\n\n@if (selectedFiles().length > 0) {\n <ul class=\"file-upload__list\">\n @for (f of selectedFiles(); track f.name; let i = $index) {\n <li class=\"file-upload__file\">\n <lc-icon name=\"document\" class=\"file-upload__file-icon\" [decorative]=\"true\"></lc-icon>\n <span class=\"file-upload__file-name\">{{ f.name }}</span>\n <span class=\"file-upload__file-size\">{{ formatSize(f.size) }}</span>\n <button\n type=\"button\"\n class=\"file-upload__file-remove\"\n (click)=\"removeFile(i); $event.stopPropagation()\"\n [attr.aria-label]=\"removeFileAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" [decorative]=\"true\"></lc-icon>\n </button>\n </li>\n }\n </ul>\n}\n",
|
|
205
205
|
styles: [":host{display:block;width:100%}.file-upload{display:flex;align-items:center;justify-content:center;padding:2rem 1.5rem;border:2px dashed var(--color-border-strong);border-radius:var(--border-radius-lg);background-color:var(--color-surface-2);cursor:pointer;transition:border-color .15s ease,background-color .15s ease;text-align:center}.file-upload:hover{border-color:var(--color-primary-500);background-color:var(--color-surface-hover)}.file-upload:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.file-upload--drag-over{border-color:var(--color-primary-500);background-color:var(--color-surface-selected)}.file-upload--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.file-upload__input{position:absolute;width:0;height:0;overflow:hidden;opacity:0}.file-upload__content{display:flex;flex-direction:column;align-items:center;gap:.5rem}.file-upload__icon{--icon-size: 2rem;color:var(--color-primary-500)}.file-upload__hint{margin:0;font-size:.875rem;color:var(--color-text-secondary)}.file-upload__accept{margin:0;font-size:.75rem;color:var(--color-text-tertiary)}.file-upload__list{list-style:none;margin:.75rem 0 0;padding:0;display:flex;flex-direction:column;gap:.5rem}.file-upload__file{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--border-radius-md);font-size:.8125rem}.file-upload__file-icon{--icon-size: 1rem;color:var(--color-text-tertiary);flex-shrink:0}.file-upload__file-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--color-text-primary)}.file-upload__file-size{flex-shrink:0;color:var(--color-text-tertiary);font-size:.75rem}.file-upload__file-remove{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;border:none;border-radius:50%;background:var(--color-surface-hover);cursor:pointer;padding:0;--icon-size: .75rem;color:var(--color-text-secondary)}.file-upload__file-remove:hover{background:#9d0e0e2e;color:var(--color-error)}\n"]
|
|
206
206
|
}]
|
|
207
207
|
}],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-file-upload.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/file-upload/file-upload.component.ts","../../../../libs/angular-ui-kit/src/lib/file-upload/file-upload.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n output,\n signal,\n computed,\n ElementRef,\n viewChild,\n} from '@angular/core';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_FILE, ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\n\nexport interface FileUploadFile {\n file: File;\n name: string;\n size: number;\n type: string;\n}\n\n/**\n * File upload / dropzone component with drag & drop support.\n *\n * @example\n * ```html\n * <lc-file-upload\n * accept=\".pdf,.png,.jpg\"\n * [multiple]=\"true\"\n * [maxSizeBytes]=\"5242880\"\n * (filesSelected)=\"onFiles($event)\"\n * ></lc-file-upload>\n * ```\n */\n@Component({\n selector: 'lc-file-upload',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_FILE, ICON_X_MARK])],\n templateUrl: './file-upload.component.html',\n styleUrls: ['./file-upload.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * File upload component with drag-and-drop support.\n *\n * Features:\n * - Drag-and-drop file dropzone with visual feedback\n * - Click-to-browse file selection\n * - File type and size validation\n * - Multi-file upload support\n * - File list display with removal\n * - Disabled state support\n *\n * @example\n * ```html\n * <lc-file-upload accept=\".pdf,.jpg\" [multiple]=\"true\" [maxSizeBytes]=\"5242880\" />\n * ```\n */\nexport class FileUploadComponent {\n /**\n * Accepted file types (e.g. \".pdf,.jpg,image/*\").\n */\n accept = input<string>('');\n\n /**\n * Allow multiple files.\n * @default false\n */\n multiple = input<boolean>(false);\n\n /**\n * Maximum file size in bytes. 0 for unlimited.\n * @default 0\n */\n maxSizeBytes = input<number>(0);\n\n /**\n * Whether the dropzone is disabled.\n * @default false\n */\n disabled = input<boolean>(false);\n\n /**\n * Hint text shown in the dropzone. The drop zone's accessible name is\n * `hint`, then the shown accepted and max lines, comma-separated (WCAG 2.5.3\n * Label in Name).\n * @default 'Drag & drop files here, or click to browse'\n */\n hint = input<string>('Drag & drop files here, or click to browse');\n\n /**\n * Prefix of the accepted file types line (`accept`). The shown line is part\n * of the drop zone's accessible name, after `hint`.\n * @default 'Accepted:'\n */\n acceptedLabel = input('Accepted:');\n\n /**\n * Prefix of the maximum file size line (`maxSizeBytes`). The shown line is\n * part of the drop zone's accessible name, after `hint`.\n * @default 'Max:'\n */\n maxSizeLabel = input('Max:');\n\n /** Accessible name of a selected file's remove button. */\n removeFileAriaLabel = input('Remove file');\n\n /**\n * Emitted with selected files after validation.\n */\n readonly filesSelected = output<FileUploadFile[]>();\n\n /**\n * Emitted when a file is rejected (too large or wrong type).\n */\n readonly fileRejected = output<{ file: File; reason: string }>();\n\n protected isDragOver = signal(false);\n protected selectedFiles = signal<FileUploadFile[]>([]);\n protected readonly fileInputRef = viewChild<ElementRef<HTMLInputElement>>('fileInput');\n\n protected dropzoneClasses = computed(() => {\n return [\n 'file-upload',\n this.isDragOver() ? 'file-upload--drag-over' : '',\n this.disabled() ? 'file-upload--disabled' : '',\n ]\n .filter(Boolean)\n .join(' ');\n });\n\n /**\n * Name of the drop zone (AS-33, WCAG 2.5.3 Label in Name): `hint`, then the\n * accepted and max lines it shows, comma-separated.\n */\n protected readonly dropzoneAriaLabel = computed(() =>\n [\n this.hint(),\n this.accept() ? `${this.acceptedLabel()} ${this.accept()}` : '',\n this.maxSizeBytes() > 0 ? `${this.maxSizeLabel()} ${this.formatSize(this.maxSizeBytes())}` : '',\n ]\n .filter(Boolean)\n .join(', '),\n );\n\n protected onDragOver(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n if (!this.disabled()) {\n this.isDragOver.set(true);\n }\n }\n\n protected onDragLeave(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n }\n\n protected onDrop(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n if (this.disabled() || !event.dataTransfer?.files.length) return;\n this.processFiles(event.dataTransfer.files);\n }\n\n protected onFileChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n if (input.files?.length) {\n this.processFiles(input.files);\n }\n // Reset so the same file can be re-selected\n input.value = '';\n }\n\n protected openFilePicker(): void {\n if (!this.disabled()) {\n this.fileInputRef()?.nativeElement.click();\n }\n }\n\n protected removeFile(index: number): void {\n this.selectedFiles.update((files) => files.filter((_, i) => i !== index));\n this.filesSelected.emit(this.selectedFiles());\n }\n\n protected formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n\n private processFiles(fileList: FileList): void {\n const files = Array.from(fileList);\n const accepted: FileUploadFile[] = [];\n\n for (const file of files) {\n if (this.maxSizeBytes() > 0 && file.size > this.maxSizeBytes()) {\n this.fileRejected.emit({ file, reason: 'File too large' });\n continue;\n }\n accepted.push({\n file,\n name: file.name,\n size: file.size,\n type: file.type,\n });\n }\n\n if (this.multiple()) {\n this.selectedFiles.update((prev) => [...prev, ...accepted]);\n } else {\n this.selectedFiles.set(accepted.slice(0, 1));\n }\n\n this.filesSelected.emit(this.selectedFiles());\n }\n}\n","<div\n [class]=\"dropzoneClasses()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (click)=\"openFilePicker()\"\n role=\"button\"\n tabindex=\"0\"\n (keydown.enter)=\"openFilePicker()\"\n (keydown.space)=\"openFilePicker()\"\n [attr.aria-label]=\"dropzoneAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n <input\n #fileInput\n type=\"file\"\n class=\"file-upload__input\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileChange($event)\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n <div class=\"file-upload__content\">\n <lc-icon name=\"cloud-arrow-up\" class=\"file-upload__icon\"></lc-icon>\n <!-- Real spaces between the lines, so the visible text reads as words\n that the drop zone's name contains (WCAG 2.5.3). -->\n <p class=\"file-upload__hint\">{{ hint() }}</p>\n @if (accept()) {\n &ngsp;<p class=\"file-upload__accept\">{{ acceptedLabel() }} {{ accept() }}</p>\n }\n @if (maxSizeBytes() > 0) {\n &ngsp;<p class=\"file-upload__accept\">{{ maxSizeLabel() }} {{ formatSize(maxSizeBytes()) }}</p>\n }\n </div>\n</div>\n\n@if (selectedFiles().length > 0) {\n <ul class=\"file-upload__list\">\n @for (f of selectedFiles(); track f.name; let i = $index) {\n <li class=\"file-upload__file\">\n <lc-icon name=\"document\" class=\"file-upload__file-icon\"></lc-icon>\n <span class=\"file-upload__file-name\">{{ f.name }}</span>\n <span class=\"file-upload__file-size\">{{ formatSize(f.size) }}</span>\n <button\n type=\"button\"\n class=\"file-upload__file-remove\"\n (click)=\"removeFile(i); $event.stopPropagation()\"\n [attr.aria-label]=\"removeFileAriaLabel()\"\n >\n <lc-icon name=\"x-mark\"></lc-icon>\n </button>\n </li>\n }\n </ul>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-file-upload.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/file-upload/file-upload.component.ts","../../../../libs/angular-ui-kit/src/lib/file-upload/file-upload.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n output,\n signal,\n computed,\n ElementRef,\n viewChild,\n} from '@angular/core';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_FILE, ICON_X_MARK } from '@lifecorner/angular-ui-kit/icons';\n\nexport interface FileUploadFile {\n file: File;\n name: string;\n size: number;\n type: string;\n}\n\n/**\n * File upload / dropzone component with drag & drop support.\n *\n * @example\n * ```html\n * <lc-file-upload\n * accept=\".pdf,.png,.jpg\"\n * [multiple]=\"true\"\n * [maxSizeBytes]=\"5242880\"\n * (filesSelected)=\"onFiles($event)\"\n * ></lc-file-upload>\n * ```\n */\n@Component({\n selector: 'lc-file-upload',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_FILE, ICON_X_MARK])],\n templateUrl: './file-upload.component.html',\n styleUrls: ['./file-upload.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * File upload component with drag-and-drop support.\n *\n * Features:\n * - Drag-and-drop file dropzone with visual feedback\n * - Click-to-browse file selection\n * - File type and size validation\n * - Multi-file upload support\n * - File list display with removal\n * - Disabled state support\n *\n * @example\n * ```html\n * <lc-file-upload accept=\".pdf,.jpg\" [multiple]=\"true\" [maxSizeBytes]=\"5242880\" />\n * ```\n */\nexport class FileUploadComponent {\n /**\n * Accepted file types (e.g. \".pdf,.jpg,image/*\").\n */\n accept = input<string>('');\n\n /**\n * Allow multiple files.\n * @default false\n */\n multiple = input<boolean>(false);\n\n /**\n * Maximum file size in bytes. 0 for unlimited.\n * @default 0\n */\n maxSizeBytes = input<number>(0);\n\n /**\n * Whether the dropzone is disabled.\n * @default false\n */\n disabled = input<boolean>(false);\n\n /**\n * Hint text shown in the dropzone. The drop zone's accessible name is\n * `hint`, then the shown accepted and max lines, comma-separated (WCAG 2.5.3\n * Label in Name).\n * @default 'Drag & drop files here, or click to browse'\n */\n hint = input<string>('Drag & drop files here, or click to browse');\n\n /**\n * Prefix of the accepted file types line (`accept`). The shown line is part\n * of the drop zone's accessible name, after `hint`.\n * @default 'Accepted:'\n */\n acceptedLabel = input('Accepted:');\n\n /**\n * Prefix of the maximum file size line (`maxSizeBytes`). The shown line is\n * part of the drop zone's accessible name, after `hint`.\n * @default 'Max:'\n */\n maxSizeLabel = input('Max:');\n\n /** Accessible name of a selected file's remove button. */\n removeFileAriaLabel = input('Remove file');\n\n /**\n * Emitted with selected files after validation.\n */\n readonly filesSelected = output<FileUploadFile[]>();\n\n /**\n * Emitted when a file is rejected (too large or wrong type).\n */\n readonly fileRejected = output<{ file: File; reason: string }>();\n\n protected isDragOver = signal(false);\n protected selectedFiles = signal<FileUploadFile[]>([]);\n protected readonly fileInputRef = viewChild<ElementRef<HTMLInputElement>>('fileInput');\n\n protected dropzoneClasses = computed(() => {\n return [\n 'file-upload',\n this.isDragOver() ? 'file-upload--drag-over' : '',\n this.disabled() ? 'file-upload--disabled' : '',\n ]\n .filter(Boolean)\n .join(' ');\n });\n\n /**\n * Name of the drop zone (AS-33, WCAG 2.5.3 Label in Name): `hint`, then the\n * accepted and max lines it shows, comma-separated.\n */\n protected readonly dropzoneAriaLabel = computed(() =>\n [\n this.hint(),\n this.accept() ? `${this.acceptedLabel()} ${this.accept()}` : '',\n this.maxSizeBytes() > 0 ? `${this.maxSizeLabel()} ${this.formatSize(this.maxSizeBytes())}` : '',\n ]\n .filter(Boolean)\n .join(', '),\n );\n\n protected onDragOver(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n if (!this.disabled()) {\n this.isDragOver.set(true);\n }\n }\n\n protected onDragLeave(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n }\n\n protected onDrop(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n if (this.disabled() || !event.dataTransfer?.files.length) return;\n this.processFiles(event.dataTransfer.files);\n }\n\n protected onFileChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n if (input.files?.length) {\n this.processFiles(input.files);\n }\n // Reset so the same file can be re-selected\n input.value = '';\n }\n\n protected openFilePicker(): void {\n if (!this.disabled()) {\n this.fileInputRef()?.nativeElement.click();\n }\n }\n\n protected removeFile(index: number): void {\n this.selectedFiles.update((files) => files.filter((_, i) => i !== index));\n this.filesSelected.emit(this.selectedFiles());\n }\n\n protected formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n\n private processFiles(fileList: FileList): void {\n const files = Array.from(fileList);\n const accepted: FileUploadFile[] = [];\n\n for (const file of files) {\n if (this.maxSizeBytes() > 0 && file.size > this.maxSizeBytes()) {\n this.fileRejected.emit({ file, reason: 'File too large' });\n continue;\n }\n accepted.push({\n file,\n name: file.name,\n size: file.size,\n type: file.type,\n });\n }\n\n if (this.multiple()) {\n this.selectedFiles.update((prev) => [...prev, ...accepted]);\n } else {\n this.selectedFiles.set(accepted.slice(0, 1));\n }\n\n this.filesSelected.emit(this.selectedFiles());\n }\n}\n","<div\n [class]=\"dropzoneClasses()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (click)=\"openFilePicker()\"\n role=\"button\"\n tabindex=\"0\"\n (keydown.enter)=\"openFilePicker()\"\n (keydown.space)=\"openFilePicker()\"\n [attr.aria-label]=\"dropzoneAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n <input\n #fileInput\n type=\"file\"\n class=\"file-upload__input\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileChange($event)\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n <div class=\"file-upload__content\">\n <lc-icon name=\"cloud-arrow-up\" class=\"file-upload__icon\" [decorative]=\"true\"></lc-icon>\n <!-- Real spaces between the lines, so the visible text reads as words\n that the drop zone's name contains (WCAG 2.5.3). -->\n <p class=\"file-upload__hint\">{{ hint() }}</p>\n @if (accept()) {\n &ngsp;<p class=\"file-upload__accept\">{{ acceptedLabel() }} {{ accept() }}</p>\n }\n @if (maxSizeBytes() > 0) {\n &ngsp;<p class=\"file-upload__accept\">{{ maxSizeLabel() }} {{ formatSize(maxSizeBytes()) }}</p>\n }\n </div>\n</div>\n\n@if (selectedFiles().length > 0) {\n <ul class=\"file-upload__list\">\n @for (f of selectedFiles(); track f.name; let i = $index) {\n <li class=\"file-upload__file\">\n <lc-icon name=\"document\" class=\"file-upload__file-icon\" [decorative]=\"true\"></lc-icon>\n <span class=\"file-upload__file-name\">{{ f.name }}</span>\n <span class=\"file-upload__file-size\">{{ formatSize(f.size) }}</span>\n <button\n type=\"button\"\n class=\"file-upload__file-remove\"\n (click)=\"removeFile(i); $event.stopPropagation()\"\n [attr.aria-label]=\"removeFileAriaLabel()\"\n >\n <lc-icon name=\"x-mark\" [decorative]=\"true\"></lc-icon>\n </button>\n </li>\n }\n </ul>\n}\n"],"mappings":"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"}
|
|
@@ -346,7 +346,7 @@ var GalleryComponent = class GalleryComponent {
|
|
|
346
346
|
isSignal: true
|
|
347
347
|
}],
|
|
348
348
|
ngImport: i0,
|
|
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\" />\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\" />\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\" />\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\" />\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\" />\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\" />\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\" />\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",
|
|
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
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"],
|
|
351
351
|
dependencies: [
|
|
352
352
|
{
|
|
@@ -409,7 +409,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
409
409
|
])],
|
|
410
410
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
411
411
|
host: { "[class]": "\"gallery\"" },
|
|
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\" />\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\" />\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\" />\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\" />\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\" />\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\" />\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\" />\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",
|
|
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
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"]
|
|
414
414
|
}]
|
|
415
415
|
}],
|
|
@@ -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\" />\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\" />\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\" />\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\" />\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\" />\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\" />\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\" />\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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"}
|