@lifecorner/angular-ui-kit 3.11.4 → 3.11.5
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-accordion.mjs +6 -3
- package/fesm2022/lifecorner-angular-ui-kit-accordion.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-calendar.mjs +11 -6
- package/fesm2022/lifecorner-angular-ui-kit-calendar.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs +7 -2
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs +7 -2
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs +4 -2
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs +20 -0
- package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs.map +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-pagination.mjs +5 -2
- package/fesm2022/lifecorner-angular-ui-kit-pagination.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs +6 -2
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-stepper.mjs +11 -6
- package/fesm2022/lifecorner-angular-ui-kit-stepper.mjs.map +1 -1
- package/package.json +5 -1
- package/types/lifecorner-angular-ui-kit-accordion.d.ts +31 -9
- package/types/lifecorner-angular-ui-kit-calendar.d.ts +1 -0
- package/types/lifecorner-angular-ui-kit-file-upload.d.ts +18 -3
- package/types/lifecorner-angular-ui-kit-gallery.d.ts +17 -2
- package/types/lifecorner-angular-ui-kit-header.d.ts +12 -1
- package/types/lifecorner-angular-ui-kit-internal-a11y.d.ts +27 -0
- package/types/lifecorner-angular-ui-kit-pagination.d.ts +15 -2
- package/types/lifecorner-angular-ui-kit-rich-text-editor.d.ts +13 -1
- package/types/lifecorner-angular-ui-kit-stepper.d.ts +16 -5
|
@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, Directive, TemplateRef, computed, c
|
|
|
3
3
|
import { NgTemplateOutlet } from "@angular/common";
|
|
4
4
|
import { IconComponent, ɵprovideKitIcons } from "@lifecorner/angular-ui-kit/icon";
|
|
5
5
|
import { ICON_CHEVRON_DOWN } from "@lifecorner/angular-ui-kit/icons";
|
|
6
|
+
import { nameWithText } from "@lifecorner/angular-ui-kit/internal/a11y";
|
|
6
7
|
var AccordionHeaderDirective = class AccordionHeaderDirective {
|
|
7
8
|
template = inject(TemplateRef);
|
|
8
9
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
@@ -89,7 +90,9 @@ var AccordionComponent = class AccordionComponent {
|
|
|
89
90
|
if (this.expanded()) this.hasBeenOpened.set(true);
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
richHeaderLabel = computed(() => this.headerTemplate() ? (this.ariaLabel() || this.title()).trim() : "", ...ngDevMode ? [{ debugName: "richHeaderLabel" }] : /* istanbul ignore next */ []);
|
|
94
|
+
headerAriaLabel = computed(() => this.headerTemplate() ? null : nameWithText(this.ariaLabel() || this.title(), this.title()) || null, ...ngDevMode ? [{ debugName: "headerAriaLabel" }] : /* istanbul ignore next */ []);
|
|
95
|
+
headerLabelledBy = computed(() => this.richHeaderLabel() ? `${this.headerId}-label ${this.headerId}-content` : null, ...ngDevMode ? [{ debugName: "headerLabelledBy" }] : /* istanbul ignore next */ []);
|
|
93
96
|
shouldRenderBody = computed(() => {
|
|
94
97
|
if (this.destroyOnClose()) return this.expanded();
|
|
95
98
|
if (this.lazy()) return this.hasBeenOpened();
|
|
@@ -217,7 +220,7 @@ var AccordionComponent = class AccordionComponent {
|
|
|
217
220
|
isSignal: true
|
|
218
221
|
}],
|
|
219
222
|
ngImport: i0,
|
|
220
|
-
template: "<div [class]=\"accordionClasses()\">\n <!-- The heading wrapper gives the disclosure button a heading level (WAI-ARIA accordion pattern). -->\n <div class=\"lc-accordion__heading\" role=\"heading\" [attr.aria-level]=\"headingLevel()\">\n <button\n class=\"lc-accordion__header\"\n type=\"button\"\n [id]=\"headerId\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (headerTemplate(); as header) {\n <span class=\"lc-accordion__header-content\">\n <ng-container *ngTemplateOutlet=\"header.template\"></ng-container>\n </span>\n } @else {\n <span class=\"lc-accordion__title\">{{ title() }}</span>\n }\n <lc-icon\n class=\"lc-accordion__icon\"\n name=\"chevron-down\"\n size=\"sm\"\n [decorative]=\"true\"\n ></lc-icon>\n </button>\n </div>\n\n <!--\n A collapsed panel is `inert`: its eagerly projected body stays in the DOM\n (for the height animation) but is neither focusable nor exposed to AT.\n -->\n <div\n class=\"lc-accordion__panel\"\n [id]=\"panelId\"\n role=\"region\"\n [attr.aria-labelledby]=\"headerId\"\n [attr.aria-hidden]=\"!expanded()\"\n [attr.inert]=\"expanded() ? null : ''\"\n >\n <div class=\"lc-accordion__body\">\n @if (contentTemplate(); as content) {\n @if (shouldRenderBody()) {\n <ng-container *ngTemplateOutlet=\"content.template\"></ng-container>\n }\n } @else {\n <ng-content></ng-content>\n }\n </div>\n </div>\n</div>\n",
|
|
223
|
+
template: "<div [class]=\"accordionClasses()\">\n <!-- The heading wrapper gives the disclosure button a heading level (WAI-ARIA accordion pattern). -->\n <div class=\"lc-accordion__heading\" role=\"heading\" [attr.aria-level]=\"headingLevel()\">\n <button\n class=\"lc-accordion__header\"\n type=\"button\"\n [id]=\"headerId\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-labelledby]=\"headerLabelledBy()\"\n [attr.aria-disabled]=\"disabled()\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (headerTemplate(); as header) {\n <!-- The label starts the name and the template's text follows it, so\n the name contains what is shown (WCAG 2.5.3). -->\n @if (richHeaderLabel()) {\n <span hidden [id]=\"headerId + '-label'\">{{ richHeaderLabel() }},</span>\n }\n <span class=\"lc-accordion__header-content\" [attr.id]=\"richHeaderLabel() ? headerId + '-content' : null\">\n <ng-container *ngTemplateOutlet=\"header.template\"></ng-container>\n </span>\n } @else {\n <span class=\"lc-accordion__title\">{{ title() }}</span>\n }\n <lc-icon\n class=\"lc-accordion__icon\"\n name=\"chevron-down\"\n size=\"sm\"\n [decorative]=\"true\"\n ></lc-icon>\n </button>\n </div>\n\n <!--\n A collapsed panel is `inert`: its eagerly projected body stays in the DOM\n (for the height animation) but is neither focusable nor exposed to AT.\n -->\n <div\n class=\"lc-accordion__panel\"\n [id]=\"panelId\"\n role=\"region\"\n [attr.aria-labelledby]=\"headerLabelledBy() ?? headerId\"\n [attr.aria-hidden]=\"!expanded()\"\n [attr.inert]=\"expanded() ? null : ''\"\n >\n <div class=\"lc-accordion__body\">\n @if (contentTemplate(); as content) {\n @if (shouldRenderBody()) {\n <ng-container *ngTemplateOutlet=\"content.template\"></ng-container>\n }\n } @else {\n <ng-content></ng-content>\n }\n </div>\n </div>\n</div>\n",
|
|
221
224
|
styles: [":host{display:block}.lc-accordion{border-radius:var(--border-radius-lg);overflow:hidden;transition:box-shadow .2s ease,border-color .2s ease}.lc-accordion--outlined{border:1px solid var(--lc-accordion-border, var(--color-border));background:var(--color-surface);box-shadow:var(--elevation-1)}.lc-accordion--outlined:hover:not(.lc-accordion--disabled){box-shadow:var(--elevation-1)}.lc-accordion--flat{border:none;background:transparent}.lc-accordion--disabled{opacity:.5;pointer-events:none}.lc-accordion__heading{display:block;margin:0}.lc-accordion__header{display:flex;width:100%;align-items:center;justify-content:space-between;gap:.75rem;padding:var(--lc-accordion-header-padding, .875rem 1.125rem);font-weight:600;letter-spacing:-.01em;color:var(--lc-accordion-header-fg, var(--color-text-primary));background:var(--lc-accordion-header-bg, transparent);border:var(--lc-accordion-header-border, none);border-radius:0;cursor:pointer;transition:background-color .15s ease,color .15s ease}.lc-accordion__header:hover{background-color:var(--lc-accordion-header-hover-bg, var(--color-surface-hover))}.lc-accordion__header:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;border-radius:var(--border-radius-md)}.lc-accordion__header:disabled{cursor:not-allowed}.lc-accordion--sm .lc-accordion__header{padding:var(--lc-accordion-header-padding, .625rem .875rem);font-size:.8125rem}.lc-accordion--md .lc-accordion__header{padding:var(--lc-accordion-header-padding, .875rem 1.125rem);font-size:.9375rem}.lc-accordion--lg .lc-accordion__header{padding:var(--lc-accordion-header-padding, 1.125rem 1.375rem);font-size:1.0625rem}.lc-accordion__title{flex:1;text-align:left}.lc-accordion__header-content{display:flex;flex:1;min-width:0;align-items:center;gap:.75rem;text-align:left;font-weight:inherit}.lc-accordion__icon{flex-shrink:0;color:var(--color-text-secondary);transition:transform .3s cubic-bezier(.25,.46,.45,.94),color .2s ease}.lc-accordion--chevron-leading .lc-accordion__icon{order:-1}.lc-accordion__header:hover .lc-accordion__icon{color:var(--color-text-primary)}.lc-accordion--expanded .lc-accordion__icon{transform:rotate(-180deg)}.lc-accordion__panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows .3s cubic-bezier(.25,.46,.45,.94);overflow:hidden}.lc-accordion--expanded .lc-accordion__panel{grid-template-rows:1fr}.lc-accordion__body{min-height:0;overflow:hidden;padding:0 1.125rem;color:var(--color-text-secondary);font-size:.875rem;line-height:1.6;opacity:0;transform:translateY(-4px);visibility:hidden;transition:opacity .2s ease 50ms,transform .2s ease 50ms,padding .3s cubic-bezier(.25,.46,.45,.94),visibility 0s linear .3s}.lc-accordion--expanded .lc-accordion__body{padding:var(--lc-accordion-body-padding, .875rem 1.125rem 1rem);opacity:1;transform:translateY(0);visibility:visible;transition:opacity .25s ease 80ms,transform .25s ease 80ms,padding .3s cubic-bezier(.25,.46,.45,.94),visibility 0s}.lc-accordion--sm .lc-accordion__body{padding:0 .875rem;font-size:.8125rem}.lc-accordion--sm.lc-accordion--expanded .lc-accordion__body{padding:var(--lc-accordion-body-padding, .625rem .875rem .75rem)}.lc-accordion--lg .lc-accordion__body{padding:0 1.375rem;font-size:.9375rem}.lc-accordion--lg.lc-accordion--expanded .lc-accordion__body{padding:var(--lc-accordion-body-padding, 1rem 1.375rem 1.25rem)}\n"],
|
|
222
225
|
dependencies: [{
|
|
223
226
|
kind: "component",
|
|
@@ -259,7 +262,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
259
262
|
imports: [IconComponent, NgTemplateOutlet],
|
|
260
263
|
viewProviders: [ɵprovideKitIcons([ICON_CHEVRON_DOWN])],
|
|
261
264
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
262
|
-
template: "<div [class]=\"accordionClasses()\">\n <!-- The heading wrapper gives the disclosure button a heading level (WAI-ARIA accordion pattern). -->\n <div class=\"lc-accordion__heading\" role=\"heading\" [attr.aria-level]=\"headingLevel()\">\n <button\n class=\"lc-accordion__header\"\n type=\"button\"\n [id]=\"headerId\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (headerTemplate(); as header) {\n <span class=\"lc-accordion__header-content\">\n <ng-container *ngTemplateOutlet=\"header.template\"></ng-container>\n </span>\n } @else {\n <span class=\"lc-accordion__title\">{{ title() }}</span>\n }\n <lc-icon\n class=\"lc-accordion__icon\"\n name=\"chevron-down\"\n size=\"sm\"\n [decorative]=\"true\"\n ></lc-icon>\n </button>\n </div>\n\n <!--\n A collapsed panel is `inert`: its eagerly projected body stays in the DOM\n (for the height animation) but is neither focusable nor exposed to AT.\n -->\n <div\n class=\"lc-accordion__panel\"\n [id]=\"panelId\"\n role=\"region\"\n [attr.aria-labelledby]=\"headerId\"\n [attr.aria-hidden]=\"!expanded()\"\n [attr.inert]=\"expanded() ? null : ''\"\n >\n <div class=\"lc-accordion__body\">\n @if (contentTemplate(); as content) {\n @if (shouldRenderBody()) {\n <ng-container *ngTemplateOutlet=\"content.template\"></ng-container>\n }\n } @else {\n <ng-content></ng-content>\n }\n </div>\n </div>\n</div>\n",
|
|
265
|
+
template: "<div [class]=\"accordionClasses()\">\n <!-- The heading wrapper gives the disclosure button a heading level (WAI-ARIA accordion pattern). -->\n <div class=\"lc-accordion__heading\" role=\"heading\" [attr.aria-level]=\"headingLevel()\">\n <button\n class=\"lc-accordion__header\"\n type=\"button\"\n [id]=\"headerId\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-labelledby]=\"headerLabelledBy()\"\n [attr.aria-disabled]=\"disabled()\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (headerTemplate(); as header) {\n <!-- The label starts the name and the template's text follows it, so\n the name contains what is shown (WCAG 2.5.3). -->\n @if (richHeaderLabel()) {\n <span hidden [id]=\"headerId + '-label'\">{{ richHeaderLabel() }},</span>\n }\n <span class=\"lc-accordion__header-content\" [attr.id]=\"richHeaderLabel() ? headerId + '-content' : null\">\n <ng-container *ngTemplateOutlet=\"header.template\"></ng-container>\n </span>\n } @else {\n <span class=\"lc-accordion__title\">{{ title() }}</span>\n }\n <lc-icon\n class=\"lc-accordion__icon\"\n name=\"chevron-down\"\n size=\"sm\"\n [decorative]=\"true\"\n ></lc-icon>\n </button>\n </div>\n\n <!--\n A collapsed panel is `inert`: its eagerly projected body stays in the DOM\n (for the height animation) but is neither focusable nor exposed to AT.\n -->\n <div\n class=\"lc-accordion__panel\"\n [id]=\"panelId\"\n role=\"region\"\n [attr.aria-labelledby]=\"headerLabelledBy() ?? headerId\"\n [attr.aria-hidden]=\"!expanded()\"\n [attr.inert]=\"expanded() ? null : ''\"\n >\n <div class=\"lc-accordion__body\">\n @if (contentTemplate(); as content) {\n @if (shouldRenderBody()) {\n <ng-container *ngTemplateOutlet=\"content.template\"></ng-container>\n }\n } @else {\n <ng-content></ng-content>\n }\n </div>\n </div>\n</div>\n",
|
|
263
266
|
styles: [":host{display:block}.lc-accordion{border-radius:var(--border-radius-lg);overflow:hidden;transition:box-shadow .2s ease,border-color .2s ease}.lc-accordion--outlined{border:1px solid var(--lc-accordion-border, var(--color-border));background:var(--color-surface);box-shadow:var(--elevation-1)}.lc-accordion--outlined:hover:not(.lc-accordion--disabled){box-shadow:var(--elevation-1)}.lc-accordion--flat{border:none;background:transparent}.lc-accordion--disabled{opacity:.5;pointer-events:none}.lc-accordion__heading{display:block;margin:0}.lc-accordion__header{display:flex;width:100%;align-items:center;justify-content:space-between;gap:.75rem;padding:var(--lc-accordion-header-padding, .875rem 1.125rem);font-weight:600;letter-spacing:-.01em;color:var(--lc-accordion-header-fg, var(--color-text-primary));background:var(--lc-accordion-header-bg, transparent);border:var(--lc-accordion-header-border, none);border-radius:0;cursor:pointer;transition:background-color .15s ease,color .15s ease}.lc-accordion__header:hover{background-color:var(--lc-accordion-header-hover-bg, var(--color-surface-hover))}.lc-accordion__header:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px;border-radius:var(--border-radius-md)}.lc-accordion__header:disabled{cursor:not-allowed}.lc-accordion--sm .lc-accordion__header{padding:var(--lc-accordion-header-padding, .625rem .875rem);font-size:.8125rem}.lc-accordion--md .lc-accordion__header{padding:var(--lc-accordion-header-padding, .875rem 1.125rem);font-size:.9375rem}.lc-accordion--lg .lc-accordion__header{padding:var(--lc-accordion-header-padding, 1.125rem 1.375rem);font-size:1.0625rem}.lc-accordion__title{flex:1;text-align:left}.lc-accordion__header-content{display:flex;flex:1;min-width:0;align-items:center;gap:.75rem;text-align:left;font-weight:inherit}.lc-accordion__icon{flex-shrink:0;color:var(--color-text-secondary);transition:transform .3s cubic-bezier(.25,.46,.45,.94),color .2s ease}.lc-accordion--chevron-leading .lc-accordion__icon{order:-1}.lc-accordion__header:hover .lc-accordion__icon{color:var(--color-text-primary)}.lc-accordion--expanded .lc-accordion__icon{transform:rotate(-180deg)}.lc-accordion__panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows .3s cubic-bezier(.25,.46,.45,.94);overflow:hidden}.lc-accordion--expanded .lc-accordion__panel{grid-template-rows:1fr}.lc-accordion__body{min-height:0;overflow:hidden;padding:0 1.125rem;color:var(--color-text-secondary);font-size:.875rem;line-height:1.6;opacity:0;transform:translateY(-4px);visibility:hidden;transition:opacity .2s ease 50ms,transform .2s ease 50ms,padding .3s cubic-bezier(.25,.46,.45,.94),visibility 0s linear .3s}.lc-accordion--expanded .lc-accordion__body{padding:var(--lc-accordion-body-padding, .875rem 1.125rem 1rem);opacity:1;transform:translateY(0);visibility:visible;transition:opacity .25s ease 80ms,transform .25s ease 80ms,padding .3s cubic-bezier(.25,.46,.45,.94),visibility 0s}.lc-accordion--sm .lc-accordion__body{padding:0 .875rem;font-size:.8125rem}.lc-accordion--sm.lc-accordion--expanded .lc-accordion__body{padding:var(--lc-accordion-body-padding, .625rem .875rem .75rem)}.lc-accordion--lg .lc-accordion__body{padding:0 1.375rem;font-size:.9375rem}.lc-accordion--lg.lc-accordion--expanded .lc-accordion__body{padding:var(--lc-accordion-body-padding, 1rem 1.375rem 1.25rem)}\n"]
|
|
264
267
|
}]
|
|
265
268
|
}],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-accordion.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/accordion/accordion-header.directive.ts","../../../../libs/angular-ui-kit/src/lib/accordion/accordion-content.directive.ts","../../../../libs/angular-ui-kit/src/lib/accordion/accordion.component.ts","../../../../libs/angular-ui-kit/src/lib/accordion/accordion.component.html","../../../../libs/angular-ui-kit/src/lib/accordion/accordion-group.component.ts"],"sourcesContent":["import { Directive, TemplateRef, inject } from '@angular/core';\n\n/**\n * Marks an `<ng-template>` as the rich header content for an `<lc-accordion>`.\n *\n * When present, the projected template is rendered inside the header button\n * instead of the plain `title` string. The accordion still owns the disclosure\n * chevron, click/keyboard handling and focus ring — the consumer only supplies\n * the (non-interactive) inner content.\n *\n * A11y constraint: the header is itself a `<button>`, so the template must not\n * contain nested interactive elements (no button/link/input). Text, badges and\n * other non-interactive nodes only.\n *\n * @example\n * ```html\n * <lc-accordion variant=\"flat\" chevronPosition=\"leading\">\n * <ng-template lcAccordionHeader>\n * <span class=\"title\">Item label</span>\n * <lc-badge variant=\"success\" size=\"sm\">Done</lc-badge>\n * <span style=\"margin-left: auto;\">12:04</span>\n * </ng-template>\n * <div>Body…</div>\n * </lc-accordion>\n * ```\n */\n@Directive({\n selector: '[lcAccordionHeader]',\n standalone: true,\n})\nexport class AccordionHeaderDirective {\n public readonly template = inject(TemplateRef<unknown>);\n}\n","import { Directive, TemplateRef, inject } from '@angular/core';\n\n/**\n * Marks an `<ng-template>` as the (optionally lazy) body content for an\n * `<lc-accordion>`.\n *\n * Unlike default `<ng-content>` projection — which Angular always instantiates\n * eagerly — a template referenced through this directive is only stamped out\n * when the accordion decides to render its body. That is what makes\n * `[lazy]=\"true\"` and `[destroyOnClose]=\"true\"` possible: expensive children\n * (HTTP-backed panels, charts, live-polling views) are not created until the\n * panel is first opened.\n *\n * @example\n * ```html\n * <lc-accordion [lazy]=\"true\">\n * <ng-template lcAccordionContent>\n * <expensive-panel [id]=\"id\" />\n * </ng-template>\n * </lc-accordion>\n * ```\n */\n@Directive({\n selector: '[lcAccordionContent]',\n standalone: true,\n})\nexport class AccordionContentDirective {\n public readonly template = inject(TemplateRef<unknown>);\n}\n","import {\n Component,\n ChangeDetectionStrategy,\n input,\n model,\n computed,\n signal,\n contentChild,\n effect,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHEVRON_DOWN } from '@lifecorner/angular-ui-kit/icons';\nimport { AccordionHeaderDirective } from './accordion-header.directive';\nimport { AccordionContentDirective } from './accordion-content.directive';\n\n/** Unique id source for header/panel wiring (aria-controls / aria-labelledby). */\nlet accordionUid = 0;\n\n/** Where the disclosure chevron sits relative to the header content. */\nexport type AccordionChevronPosition = 'leading' | 'trailing';\n\n/**\n * Accordion component for collapsible content sections.\n *\n * Features:\n * - Expandable/collapsible content panels\n * - Two-way binding for expanded state\n * - Animated expand/collapse transitions\n * - Plain-string `title` **or** a rich projected header (`lcAccordionHeader`)\n * - Eager `<ng-content>` body **or** lazy/destroyable template body\n * (`lcAccordionContent` + `[lazy]` / `[destroyOnClose]`)\n * - Accessible: header is a real `<button>` inside a heading with\n * `aria-expanded` / `aria-controls`, keyboard support and a visible focus\n * ring; the collapsed panel is `inert` (not focusable, hidden from AT)\n *\n * @example Plain (unchanged, fully backward compatible)\n * ```html\n * <lc-accordion title=\"Section Title\" [(expanded)]=\"isOpen\">\n * <p>Collapsible content here</p>\n * </lc-accordion>\n * ```\n *\n * @example Rich header + lazy body\n * ```html\n * <lc-accordion variant=\"flat\" chevronPosition=\"leading\" [lazy]=\"true\">\n * <ng-template lcAccordionHeader>\n * <span class=\"title\">Item label</span>\n * <lc-badge variant=\"success\" size=\"sm\">Done</lc-badge>\n * <span style=\"margin-left: auto;\">12:04</span>\n * </ng-template>\n * <ng-template lcAccordionContent>\n * <expensive-panel />\n * </ng-template>\n * </lc-accordion>\n * ```\n */\n@Component({\n selector: 'lc-accordion',\n standalone: true,\n imports: [IconComponent, NgTemplateOutlet],\n viewProviders: [ɵprovideKitIcons([ICON_CHEVRON_DOWN])],\n templateUrl: './accordion.component.html',\n styleUrl: './accordion.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AccordionComponent {\n /**\n * Title displayed in the accordion header. Optional: when a\n * `lcAccordionHeader` template is projected it takes precedence, and `title`\n * (if set) becomes the header's accessible label fallback.\n */\n readonly title = input<string>('');\n\n /**\n * Explicit accessible label for the header button. Use this when the header\n * is a rich template with no meaningful plain-text title. Falls back to\n * `title` when omitted.\n */\n readonly ariaLabel = input<string>('');\n\n /** Whether the accordion is expanded (two-way binding) */\n readonly expanded = model<boolean>(false);\n\n /** Visual variant */\n readonly variant = input<'outlined' | 'flat'>('outlined');\n\n /** Size of the header */\n readonly size = input<'sm' | 'md' | 'lg'>('md');\n\n /** Whether the accordion is disabled */\n readonly disabled = input<boolean>(false);\n\n /**\n * Heading level (1–6) announced for the header button, so the accordion\n * fits into the surrounding document outline.\n * @default 3\n */\n readonly headingLevel = input<1 | 2 | 3 | 4 | 5 | 6>(3);\n\n /**\n * Where the disclosure chevron sits. `'trailing'` (default) preserves the\n * original layout. `'leading'` puts the chevron first so a right-aligned\n * header element (e.g. a timestamp using `margin-left: auto`) stays pinned to\n * the right edge.\n */\n readonly chevronPosition = input<AccordionChevronPosition>('trailing');\n\n /**\n * When `true`, a `lcAccordionContent` body is not instantiated until the\n * panel is first expanded; once created it is kept in the DOM on collapse.\n * Default `false` = today's behavior (projected immediately). Has no effect\n * on default `<ng-content>` bodies, which Angular always projects eagerly.\n */\n readonly lazy = input<boolean>(false);\n\n /**\n * When `true`, a `lcAccordionContent` body is destroyed on collapse and\n * recreated on the next open. Takes precedence over {@link lazy}.\n */\n readonly destroyOnClose = input<boolean>(false);\n\n /** Rich header template, if projected via `lcAccordionHeader`. */\n readonly headerTemplate = contentChild(AccordionHeaderDirective);\n\n /** Lazy/deferred body template, if projected via `lcAccordionContent`. */\n readonly contentTemplate = contentChild(AccordionContentDirective);\n\n /** Stable ids wiring the header button to its panel for screen readers. */\n protected readonly headerId = `lc-accordion-header-${accordionUid}`;\n protected readonly panelId = `lc-accordion-panel-${accordionUid++}`;\n\n /** Latches to `true` the first time the panel is opened (for lazy bodies). */\n private readonly hasBeenOpened = signal(false);\n\n constructor() {\n // Record the first open so lazy bodies can render (and stay) afterwards.\n effect(() => {\n if (this.expanded()) {\n this.hasBeenOpened.set(true);\n }\n });\n }\n\n /** Accessible label for the header button (explicit override, else title). */\n protected readonly headerAriaLabel = computed(\n () => this.ariaLabel() || this.title() || null\n );\n\n /**\n * Whether the `lcAccordionContent` body should currently be in the DOM.\n * - `destroyOnClose`: only while expanded.\n * - `lazy`: from the first open onward.\n * - otherwise: always (eager).\n */\n protected readonly shouldRenderBody = computed(() => {\n if (this.destroyOnClose()) return this.expanded();\n if (this.lazy()) return this.hasBeenOpened();\n return true;\n });\n\n /** Computed CSS classes */\n protected accordionClasses = computed(() => {\n const classes = [\n 'lc-accordion',\n `lc-accordion--${this.variant()}`,\n `lc-accordion--${this.size()}`,\n `lc-accordion--chevron-${this.chevronPosition()}`,\n ];\n if (this.expanded()) classes.push('lc-accordion--expanded');\n if (this.disabled()) classes.push('lc-accordion--disabled');\n return classes.join(' ');\n });\n\n /** Toggle expanded state */\n protected toggle(): void {\n if (!this.disabled()) {\n this.expanded.update((v) => !v);\n }\n }\n\n /** Handle keyboard events for accessibility */\n protected onKeydown(event: KeyboardEvent): void {\n if (this.disabled()) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.toggle();\n }\n }\n}\n","<div [class]=\"accordionClasses()\">\n <!-- The heading wrapper gives the disclosure button a heading level (WAI-ARIA accordion pattern). -->\n <div class=\"lc-accordion__heading\" role=\"heading\" [attr.aria-level]=\"headingLevel()\">\n <button\n class=\"lc-accordion__header\"\n type=\"button\"\n [id]=\"headerId\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-disabled]=\"disabled()\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (headerTemplate(); as header) {\n <span class=\"lc-accordion__header-content\">\n <ng-container *ngTemplateOutlet=\"header.template\"></ng-container>\n </span>\n } @else {\n <span class=\"lc-accordion__title\">{{ title() }}</span>\n }\n <lc-icon\n class=\"lc-accordion__icon\"\n name=\"chevron-down\"\n size=\"sm\"\n [decorative]=\"true\"\n ></lc-icon>\n </button>\n </div>\n\n <!--\n A collapsed panel is `inert`: its eagerly projected body stays in the DOM\n (for the height animation) but is neither focusable nor exposed to AT.\n -->\n <div\n class=\"lc-accordion__panel\"\n [id]=\"panelId\"\n role=\"region\"\n [attr.aria-labelledby]=\"headerId\"\n [attr.aria-hidden]=\"!expanded()\"\n [attr.inert]=\"expanded() ? null : ''\"\n >\n <div class=\"lc-accordion__body\">\n @if (contentTemplate(); as content) {\n @if (shouldRenderBody()) {\n <ng-container *ngTemplateOutlet=\"content.template\"></ng-container>\n }\n } @else {\n <ng-content></ng-content>\n }\n </div>\n </div>\n</div>\n","import {\n Component,\n ChangeDetectionStrategy,\n input,\n contentChildren,\n effect,\n untracked,\n} from '@angular/core';\nimport { AccordionComponent } from './accordion.component';\n\n@Component({\n selector: 'lc-accordion-group',\n standalone: true,\n template: '<ng-content></ng-content>',\n styleUrl: './accordion-group.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AccordionGroupComponent {\n /** When false (default), only one accordion can be expanded at a time. */\n readonly multi = input<boolean>(false);\n\n readonly accordions = contentChildren(AccordionComponent);\n\n private previousExpanded = new Set<AccordionComponent>();\n\n constructor() {\n effect(() => {\n const accordions = this.accordions();\n const multi = this.multi();\n\n // Read expanded state of every child so the effect re-runs on change\n const currentlyExpanded = accordions.filter((a) => a.expanded());\n\n if (multi) {\n this.previousExpanded = new Set(currentlyExpanded);\n return;\n }\n\n // Find newly expanded accordion\n const newlyExpanded = currentlyExpanded.find(\n (a) => !this.previousExpanded.has(a)\n );\n\n if (newlyExpanded && currentlyExpanded.length > 1) {\n untracked(() => {\n accordions.forEach((a) => {\n if (a !== newlyExpanded && a.expanded()) {\n a.expanded.set(false);\n }\n });\n });\n }\n\n this.previousExpanded = new Set(\n accordions.filter((a) => a.expanded())\n );\n });\n }\n\n /** Collapse all accordions. */\n collapseAll(): void {\n this.accordions().forEach((a) => a.expanded.set(false));\n }\n\n /** Expand all accordions (only meaningful when multi=true). */\n expandAll(): void {\n this.accordions().forEach((a) => {\n if (!a.disabled()) {\n a.expanded.set(true);\n }\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-accordion.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/accordion/accordion-header.directive.ts","../../../../libs/angular-ui-kit/src/lib/accordion/accordion-content.directive.ts","../../../../libs/angular-ui-kit/src/lib/accordion/accordion.component.ts","../../../../libs/angular-ui-kit/src/lib/accordion/accordion.component.html","../../../../libs/angular-ui-kit/src/lib/accordion/accordion-group.component.ts"],"sourcesContent":["import { Directive, TemplateRef, inject } from '@angular/core';\n\n/**\n * Marks an `<ng-template>` as the rich header content for an `<lc-accordion>`.\n *\n * When present, the projected template is rendered inside the header button\n * instead of the plain `title` string. The accordion still owns the disclosure\n * chevron, click/keyboard handling and focus ring — the consumer only supplies\n * the (non-interactive) inner content.\n *\n * A11y constraint: the header is itself a `<button>`, so the template must not\n * contain nested interactive elements (no button/link/input). Text, badges and\n * other non-interactive nodes only.\n *\n * Name (WCAG 2.5.3 Label in Name): the header is named by the template's\n * text; the accordion's `ariaLabel` (or `title`), if set, comes first through\n * `aria-labelledby`. Separate the template's parts with whitespace\n * (`&ngsp;`), so they read as words (\"Item label Done 12:04\"), and set a\n * label only for context that the template doesn't show.\n *\n * @example\n * ```html\n * <lc-accordion variant=\"flat\" chevronPosition=\"leading\">\n * <ng-template lcAccordionHeader>\n * <span class=\"title\">Item label</span>&ngsp;\n * <lc-badge variant=\"success\" size=\"sm\">Done</lc-badge>&ngsp;\n * <span style=\"margin-left: auto;\">12:04</span>\n * </ng-template>\n * <div>Body…</div>\n * </lc-accordion>\n * ```\n */\n@Directive({\n selector: '[lcAccordionHeader]',\n standalone: true,\n})\nexport class AccordionHeaderDirective {\n public readonly template = inject(TemplateRef<unknown>);\n}\n","import { Directive, TemplateRef, inject } from '@angular/core';\n\n/**\n * Marks an `<ng-template>` as the (optionally lazy) body content for an\n * `<lc-accordion>`.\n *\n * Unlike default `<ng-content>` projection — which Angular always instantiates\n * eagerly — a template referenced through this directive is only stamped out\n * when the accordion decides to render its body. That is what makes\n * `[lazy]=\"true\"` and `[destroyOnClose]=\"true\"` possible: expensive children\n * (HTTP-backed panels, charts, live-polling views) are not created until the\n * panel is first opened.\n *\n * @example\n * ```html\n * <lc-accordion [lazy]=\"true\">\n * <ng-template lcAccordionContent>\n * <expensive-panel [id]=\"id\" />\n * </ng-template>\n * </lc-accordion>\n * ```\n */\n@Directive({\n selector: '[lcAccordionContent]',\n standalone: true,\n})\nexport class AccordionContentDirective {\n public readonly template = inject(TemplateRef<unknown>);\n}\n","import {\n Component,\n ChangeDetectionStrategy,\n input,\n model,\n computed,\n signal,\n contentChild,\n effect,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHEVRON_DOWN } from '@lifecorner/angular-ui-kit/icons';\nimport { AccordionHeaderDirective } from './accordion-header.directive';\nimport { AccordionContentDirective } from './accordion-content.directive';\nimport { nameWithText } from '@lifecorner/angular-ui-kit/internal/a11y';\n\n/** Unique id source for header/panel wiring (aria-controls / aria-labelledby). */\nlet accordionUid = 0;\n\n/** Where the disclosure chevron sits relative to the header content. */\nexport type AccordionChevronPosition = 'leading' | 'trailing';\n\n/**\n * Accordion component for collapsible content sections.\n *\n * Features:\n * - Expandable/collapsible content panels\n * - Two-way binding for expanded state\n * - Animated expand/collapse transitions\n * - Plain-string `title` **or** a rich projected header (`lcAccordionHeader`)\n * - Eager `<ng-content>` body **or** lazy/destroyable template body\n * (`lcAccordionContent` + `[lazy]` / `[destroyOnClose]`)\n * - Accessible: header is a real `<button>` inside a heading with\n * `aria-expanded` / `aria-controls`, keyboard support and a visible focus\n * ring; the collapsed panel is `inert` (not focusable, hidden from AT)\n *\n * @example Plain (unchanged, fully backward compatible)\n * ```html\n * <lc-accordion title=\"Section Title\" [(expanded)]=\"isOpen\">\n * <p>Collapsible content here</p>\n * </lc-accordion>\n * ```\n *\n * @example Rich header + lazy body\n * ```html\n * <lc-accordion variant=\"flat\" chevronPosition=\"leading\" [lazy]=\"true\">\n * <ng-template lcAccordionHeader>\n * <span class=\"title\">Item label</span>&ngsp;\n * <lc-badge variant=\"success\" size=\"sm\">Done</lc-badge>&ngsp;\n * <span style=\"margin-left: auto;\">12:04</span>\n * </ng-template>\n * <ng-template lcAccordionContent>\n * <expensive-panel />\n * </ng-template>\n * </lc-accordion>\n * ```\n */\n@Component({\n selector: 'lc-accordion',\n standalone: true,\n imports: [IconComponent, NgTemplateOutlet],\n viewProviders: [ɵprovideKitIcons([ICON_CHEVRON_DOWN])],\n templateUrl: './accordion.component.html',\n styleUrl: './accordion.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AccordionComponent {\n /**\n * Title displayed in the accordion header. Optional: when a\n * `lcAccordionHeader` template is projected it takes precedence, and `title`\n * (if set) starts the header's accessible name when `ariaLabel` is empty.\n * @default ''\n */\n readonly title = input<string>('');\n\n /**\n * Start of the header button's accessible name; falls back to `title`. A\n * plain `title` is appended unless contained (WCAG 2.5.3 Label in Name).\n * With an `lcAccordionHeader` template, the name is this label (or `title`)\n * followed by the template's text, through `aria-labelledby`. Separate the\n * template's parts with whitespace (`&ngsp;`), and set a label only for\n * context that the template doesn't show.\n * @default ''\n */\n readonly ariaLabel = input<string>('');\n\n /** Whether the accordion is expanded (two-way binding) */\n readonly expanded = model<boolean>(false);\n\n /** Visual variant */\n readonly variant = input<'outlined' | 'flat'>('outlined');\n\n /** Size of the header */\n readonly size = input<'sm' | 'md' | 'lg'>('md');\n\n /** Whether the accordion is disabled */\n readonly disabled = input<boolean>(false);\n\n /**\n * Heading level (1–6) announced for the header button, so the accordion\n * fits into the surrounding document outline.\n * @default 3\n */\n readonly headingLevel = input<1 | 2 | 3 | 4 | 5 | 6>(3);\n\n /**\n * Where the disclosure chevron sits. `'trailing'` (default) preserves the\n * original layout. `'leading'` puts the chevron first so a right-aligned\n * header element (e.g. a timestamp using `margin-left: auto`) stays pinned to\n * the right edge.\n */\n readonly chevronPosition = input<AccordionChevronPosition>('trailing');\n\n /**\n * When `true`, a `lcAccordionContent` body is not instantiated until the\n * panel is first expanded; once created it is kept in the DOM on collapse.\n * Default `false` = today's behavior (projected immediately). Has no effect\n * on default `<ng-content>` bodies, which Angular always projects eagerly.\n */\n readonly lazy = input<boolean>(false);\n\n /**\n * When `true`, a `lcAccordionContent` body is destroyed on collapse and\n * recreated on the next open. Takes precedence over {@link lazy}.\n */\n readonly destroyOnClose = input<boolean>(false);\n\n /** Rich header template, if projected via `lcAccordionHeader`. */\n readonly headerTemplate = contentChild(AccordionHeaderDirective);\n\n /** Lazy/deferred body template, if projected via `lcAccordionContent`. */\n readonly contentTemplate = contentChild(AccordionContentDirective);\n\n /** Stable ids wiring the header button to its panel for screen readers. */\n protected readonly headerId = `lc-accordion-header-${accordionUid}`;\n protected readonly panelId = `lc-accordion-panel-${accordionUid++}`;\n\n /** Latches to `true` the first time the panel is opened (for lazy bodies). */\n private readonly hasBeenOpened = signal(false);\n\n constructor() {\n // Record the first open so lazy bodies can render (and stay) afterwards.\n effect(() => {\n if (this.expanded()) {\n this.hasBeenOpened.set(true);\n }\n });\n }\n\n /**\n * Label that starts a rich header's name (`ariaLabel`, else `title`); empty\n * without a header template.\n */\n protected readonly richHeaderLabel = computed(() =>\n this.headerTemplate() ? (this.ariaLabel() || this.title()).trim() : '',\n );\n\n /**\n * Accessible label of a plain header button (AS-33, WCAG 2.5.3 Label in\n * Name): the explicit override, else the title, then the visible title\n * unless contained. A rich header has none: it is named by its content.\n */\n protected readonly headerAriaLabel = computed(() =>\n this.headerTemplate() ? null : nameWithText(this.ariaLabel() || this.title(), this.title()) || null,\n );\n\n /** A labelled rich header: its hidden label, then the template's text. */\n protected readonly headerLabelledBy = computed(() =>\n this.richHeaderLabel() ? `${this.headerId}-label ${this.headerId}-content` : null,\n );\n\n /**\n * Whether the `lcAccordionContent` body should currently be in the DOM.\n * - `destroyOnClose`: only while expanded.\n * - `lazy`: from the first open onward.\n * - otherwise: always (eager).\n */\n protected readonly shouldRenderBody = computed(() => {\n if (this.destroyOnClose()) return this.expanded();\n if (this.lazy()) return this.hasBeenOpened();\n return true;\n });\n\n /** Computed CSS classes */\n protected accordionClasses = computed(() => {\n const classes = [\n 'lc-accordion',\n `lc-accordion--${this.variant()}`,\n `lc-accordion--${this.size()}`,\n `lc-accordion--chevron-${this.chevronPosition()}`,\n ];\n if (this.expanded()) classes.push('lc-accordion--expanded');\n if (this.disabled()) classes.push('lc-accordion--disabled');\n return classes.join(' ');\n });\n\n /** Toggle expanded state */\n protected toggle(): void {\n if (!this.disabled()) {\n this.expanded.update((v) => !v);\n }\n }\n\n /** Handle keyboard events for accessibility */\n protected onKeydown(event: KeyboardEvent): void {\n if (this.disabled()) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.toggle();\n }\n }\n}\n","<div [class]=\"accordionClasses()\">\n <!-- The heading wrapper gives the disclosure button a heading level (WAI-ARIA accordion pattern). -->\n <div class=\"lc-accordion__heading\" role=\"heading\" [attr.aria-level]=\"headingLevel()\">\n <button\n class=\"lc-accordion__header\"\n type=\"button\"\n [id]=\"headerId\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"headerAriaLabel()\"\n [attr.aria-labelledby]=\"headerLabelledBy()\"\n [attr.aria-disabled]=\"disabled()\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (headerTemplate(); as header) {\n <!-- The label starts the name and the template's text follows it, so\n the name contains what is shown (WCAG 2.5.3). -->\n @if (richHeaderLabel()) {\n <span hidden [id]=\"headerId + '-label'\">{{ richHeaderLabel() }},</span>\n }\n <span class=\"lc-accordion__header-content\" [attr.id]=\"richHeaderLabel() ? headerId + '-content' : null\">\n <ng-container *ngTemplateOutlet=\"header.template\"></ng-container>\n </span>\n } @else {\n <span class=\"lc-accordion__title\">{{ title() }}</span>\n }\n <lc-icon\n class=\"lc-accordion__icon\"\n name=\"chevron-down\"\n size=\"sm\"\n [decorative]=\"true\"\n ></lc-icon>\n </button>\n </div>\n\n <!--\n A collapsed panel is `inert`: its eagerly projected body stays in the DOM\n (for the height animation) but is neither focusable nor exposed to AT.\n -->\n <div\n class=\"lc-accordion__panel\"\n [id]=\"panelId\"\n role=\"region\"\n [attr.aria-labelledby]=\"headerLabelledBy() ?? headerId\"\n [attr.aria-hidden]=\"!expanded()\"\n [attr.inert]=\"expanded() ? null : ''\"\n >\n <div class=\"lc-accordion__body\">\n @if (contentTemplate(); as content) {\n @if (shouldRenderBody()) {\n <ng-container *ngTemplateOutlet=\"content.template\"></ng-container>\n }\n } @else {\n <ng-content></ng-content>\n }\n </div>\n </div>\n</div>\n","import {\n Component,\n ChangeDetectionStrategy,\n input,\n contentChildren,\n effect,\n untracked,\n} from '@angular/core';\nimport { AccordionComponent } from './accordion.component';\n\n@Component({\n selector: 'lc-accordion-group',\n standalone: true,\n template: '<ng-content></ng-content>',\n styleUrl: './accordion-group.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AccordionGroupComponent {\n /** When false (default), only one accordion can be expanded at a time. */\n readonly multi = input<boolean>(false);\n\n readonly accordions = contentChildren(AccordionComponent);\n\n private previousExpanded = new Set<AccordionComponent>();\n\n constructor() {\n effect(() => {\n const accordions = this.accordions();\n const multi = this.multi();\n\n // Read expanded state of every child so the effect re-runs on change\n const currentlyExpanded = accordions.filter((a) => a.expanded());\n\n if (multi) {\n this.previousExpanded = new Set(currentlyExpanded);\n return;\n }\n\n // Find newly expanded accordion\n const newlyExpanded = currentlyExpanded.find(\n (a) => !this.previousExpanded.has(a)\n );\n\n if (newlyExpanded && currentlyExpanded.length > 1) {\n untracked(() => {\n accordions.forEach((a) => {\n if (a !== newlyExpanded && a.expanded()) {\n a.expanded.set(false);\n }\n });\n });\n }\n\n this.previousExpanded = new Set(\n accordions.filter((a) => a.expanded())\n );\n });\n }\n\n /** Collapse all accordions. */\n collapseAll(): void {\n this.accordions().forEach((a) => a.expanded.set(false));\n }\n\n /** Expand all accordions (only meaningful when multi=true). */\n expandAll(): void {\n this.accordions().forEach((a) => {\n if (!a.disabled()) {\n a.expanded.set(true);\n }\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -103,12 +103,17 @@ var CalendarComponent = class CalendarComponent {
|
|
|
103
103
|
return Array.from({ length: 7 }, (_, i) => {
|
|
104
104
|
const day = new Date(start);
|
|
105
105
|
day.setDate(start.getDate() + i);
|
|
106
|
+
const label = day.toLocaleDateString(loc, {
|
|
107
|
+
weekday: "short",
|
|
108
|
+
day: "numeric"
|
|
109
|
+
});
|
|
106
110
|
return {
|
|
107
111
|
date: day,
|
|
108
|
-
label
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
label,
|
|
113
|
+
ariaLabel: `${label} ${day.toLocaleDateString(loc, {
|
|
114
|
+
month: "long",
|
|
115
|
+
year: "numeric"
|
|
116
|
+
})}`,
|
|
112
117
|
isToday: this.isSameDay(day, today),
|
|
113
118
|
events: evts.filter((e) => this.isEventOnDate(e, day))
|
|
114
119
|
};
|
|
@@ -267,7 +272,7 @@ var CalendarComponent = class CalendarComponent {
|
|
|
267
272
|
dateClick: "dateClick"
|
|
268
273
|
},
|
|
269
274
|
ngImport: i0,
|
|
270
|
-
template: "<div class=\"lc-calendar\">\n <!-- Header -->\n <div class=\"lc-calendar__header\">\n <div class=\"lc-calendar__nav\">\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigatePrev()\" [attr.aria-label]=\"text().previous\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10 12L6 8l4-4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <button type=\"button\" class=\"lc-calendar__today-btn\" (click)=\"navigateToday()\">{{ text().today }}</button>\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigateNext()\" [attr.aria-label]=\"text().next\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6 4l4 4-4 4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <span class=\"lc-calendar__title\" aria-live=\"polite\">{{ headerTitle() }}</span>\n </div>\n <div class=\"lc-calendar__view-toggle\" role=\"group\">\n <button type=\"button\" [class.active]=\"view() === 'day'\" [attr.aria-pressed]=\"view() === 'day'\" (click)=\"setView('day')\">{{ text().day }}</button>\n <button type=\"button\" [class.active]=\"view() === 'week'\" [attr.aria-pressed]=\"view() === 'week'\" (click)=\"setView('week')\">{{ text().week }}</button>\n <button type=\"button\" [class.active]=\"view() === 'month'\" [attr.aria-pressed]=\"view() === 'month'\" (click)=\"setView('month')\">{{ text().month }}</button>\n </div>\n </div>\n\n <!-- Month View -->\n @if (view() === 'month') {\n <div class=\"lc-calendar__month\">\n <div class=\"lc-calendar__weekdays\" aria-hidden=\"true\">\n @for (name of weekDayNames(); track name) {\n <div class=\"lc-calendar__weekday\">{{ name }}</div>\n }\n </div>\n @for (week of monthGrid(); track $index) {\n <div class=\"lc-calendar__week-row\">\n @for (cell of week; track cell.date.getTime()) {\n <!-- The cell stays a mouse target for the whole area; keyboard users reach the\n date through the day-number button and each event through its own button. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the whole cell; keyboard users use the day-number button -->\n <div\n class=\"lc-calendar__day-cell\"\n [class.lc-calendar__day-cell--today]=\"cell.isToday\"\n [class.lc-calendar__day-cell--other]=\"!cell.isCurrentMonth\"\n (click)=\"onDateClick(cell.date)\"\n >\n <button\n type=\"button\"\n class=\"lc-calendar__day-number\"\n [attr.aria-label]=\"dateLabel(cell.date)\"\n [attr.aria-current]=\"cell.isToday ? 'date' : null\"\n (click)=\"onDateClick(cell.date, $event)\"\n >\n {{ cell.date.getDate() }}\n </button>\n @for (evt of cell.events | slice:0:3; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__event-dot\"\n [class]=\"'lc-calendar__event-dot lc-calendar__event-dot--' + (evt.color || 'primary')\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n @if (cell.events.length > 3) {\n <div class=\"lc-calendar__more\">+{{ cell.events.length - 3 }}</div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Week View -->\n @if (view() === 'week') {\n <div class=\"lc-calendar__time-grid\">\n <div class=\"lc-calendar__time-header\">\n <div class=\"lc-calendar__time-gutter\"></div>\n @for (day of weekDays(); track day.date.getTime()) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-col-header\"\n [class.lc-calendar__time-col-header--today]=\"day.isToday\"\n [attr.aria-label]=\"
|
|
275
|
+
template: "<div class=\"lc-calendar\">\n <!-- Header -->\n <div class=\"lc-calendar__header\">\n <div class=\"lc-calendar__nav\">\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigatePrev()\" [attr.aria-label]=\"text().previous\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10 12L6 8l4-4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <button type=\"button\" class=\"lc-calendar__today-btn\" (click)=\"navigateToday()\">{{ text().today }}</button>\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigateNext()\" [attr.aria-label]=\"text().next\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6 4l4 4-4 4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <span class=\"lc-calendar__title\" aria-live=\"polite\">{{ headerTitle() }}</span>\n </div>\n <div class=\"lc-calendar__view-toggle\" role=\"group\">\n <button type=\"button\" [class.active]=\"view() === 'day'\" [attr.aria-pressed]=\"view() === 'day'\" (click)=\"setView('day')\">{{ text().day }}</button>\n <button type=\"button\" [class.active]=\"view() === 'week'\" [attr.aria-pressed]=\"view() === 'week'\" (click)=\"setView('week')\">{{ text().week }}</button>\n <button type=\"button\" [class.active]=\"view() === 'month'\" [attr.aria-pressed]=\"view() === 'month'\" (click)=\"setView('month')\">{{ text().month }}</button>\n </div>\n </div>\n\n <!-- Month View -->\n @if (view() === 'month') {\n <div class=\"lc-calendar__month\">\n <div class=\"lc-calendar__weekdays\" aria-hidden=\"true\">\n @for (name of weekDayNames(); track name) {\n <div class=\"lc-calendar__weekday\">{{ name }}</div>\n }\n </div>\n @for (week of monthGrid(); track $index) {\n <div class=\"lc-calendar__week-row\">\n @for (cell of week; track cell.date.getTime()) {\n <!-- The cell stays a mouse target for the whole area; keyboard users reach the\n date through the day-number button and each event through its own button. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the whole cell; keyboard users use the day-number button -->\n <div\n class=\"lc-calendar__day-cell\"\n [class.lc-calendar__day-cell--today]=\"cell.isToday\"\n [class.lc-calendar__day-cell--other]=\"!cell.isCurrentMonth\"\n (click)=\"onDateClick(cell.date)\"\n >\n <button\n type=\"button\"\n class=\"lc-calendar__day-number\"\n [attr.aria-label]=\"dateLabel(cell.date)\"\n [attr.aria-current]=\"cell.isToday ? 'date' : null\"\n (click)=\"onDateClick(cell.date, $event)\"\n >\n {{ cell.date.getDate() }}\n </button>\n @for (evt of cell.events | slice:0:3; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__event-dot\"\n [class]=\"'lc-calendar__event-dot lc-calendar__event-dot--' + (evt.color || 'primary')\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n @if (cell.events.length > 3) {\n <div class=\"lc-calendar__more\">+{{ cell.events.length - 3 }}</div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Week View -->\n @if (view() === 'week') {\n <div class=\"lc-calendar__time-grid\">\n <div class=\"lc-calendar__time-header\">\n <div class=\"lc-calendar__time-gutter\"></div>\n @for (day of weekDays(); track day.date.getTime()) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-col-header\"\n [class.lc-calendar__time-col-header--today]=\"day.isToday\"\n [attr.aria-label]=\"day.ariaLabel\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"onDateClick(day.date)\"\n >\n {{ day.label }}\n </button>\n }\n </div>\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n @for (day of weekDays(); track day.date.getTime()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col\" (click)=\"onDateClick(day.date)\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of day.events; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Day View -->\n @if (view() === 'day') {\n <div class=\"lc-calendar__time-grid lc-calendar__time-grid--day\">\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col lc-calendar__time-col--single\" (click)=\"onDateClick(currentDate())\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of dayEvents(); track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n </div>\n </div>\n }\n</div>\n",
|
|
271
276
|
styles: [".lc-calendar{font-family:var(--typography-font-family, sans-serif);border:1px solid var(--color-border);border-radius:.5rem;overflow:hidden;background:var(--color-surface)}.lc-calendar__header{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-calendar__nav{display:flex;align-items:center;gap:.5rem}.lc-calendar__time-event:focus-visible,.lc-calendar__time-col-header:focus-visible,.lc-calendar__event-dot:focus-visible,.lc-calendar__day-number:focus-visible,.lc-calendar__view-toggle button:focus-visible,.lc-calendar__today-btn:focus-visible,.lc-calendar__nav-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:1px;z-index:2}.lc-calendar__nav-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--color-border);border-radius:.375rem;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;transition:background .15s,border-color .15s}.lc-calendar__nav-btn:hover{background:var(--color-surface-hover)}.lc-calendar__today-btn{padding:.25rem .75rem;border:1px solid var(--color-border);border-radius:.375rem;background:var(--color-surface);color:var(--color-text-secondary);font-size:.8125rem;font-weight:500;cursor:pointer;transition:background .15s}.lc-calendar__today-btn:hover{background:var(--color-surface-hover)}.lc-calendar__title{font-size:1rem;font-weight:600;color:var(--color-text-primary);margin-left:.25rem}.lc-calendar__view-toggle{display:flex;border:1px solid var(--color-border);border-radius:.375rem;overflow:hidden}.lc-calendar__view-toggle button{padding:.25rem .75rem;border:none;background:var(--color-surface);color:var(--color-text-secondary);font-size:.8125rem;font-weight:500;cursor:pointer;transition:background .15s,color .15s}.lc-calendar__view-toggle button:not(:last-child){border-right:1px solid var(--color-border)}.lc-calendar__view-toggle button:hover{background:var(--color-surface-2)}.lc-calendar__view-toggle button.active{background:var(--color-primary-500);color:var(--color-on-primary)}.lc-calendar__weekdays{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--color-border)}.lc-calendar__weekday{padding:.5rem;text-align:center;font-size:.75rem;font-weight:600;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.025em}.lc-calendar__week-row{display:grid;grid-template-columns:repeat(7,1fr)}.lc-calendar__week-row:not(:last-child){border-bottom:1px solid var(--color-divider)}.lc-calendar__day-cell{min-height:5.5rem;padding:.25rem;border-right:1px solid var(--color-divider);cursor:pointer;transition:background .1s}.lc-calendar__day-cell:last-child{border-right:none}.lc-calendar__day-cell:hover{background:var(--color-surface-2)}.lc-calendar__day-cell--today{background:color-mix(in srgb,var(--color-primary-500) 6%,transparent)}.lc-calendar__day-cell--today:hover{background:color-mix(in srgb,var(--color-primary-500) 12%,transparent)}.lc-calendar__day-cell--other{opacity:.4}.lc-calendar__day-number{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:0;border:none;background:transparent;font:inherit;font-size:.75rem;font-weight:500;color:var(--color-text-secondary);border-radius:50%;cursor:pointer}.lc-calendar__day-number:hover{background:var(--color-surface-hover)}.lc-calendar__day-cell--today .lc-calendar__day-number{background:var(--color-primary-500);color:var(--color-on-primary);font-weight:700}.lc-calendar__event-dot{display:block;width:100%;border:none;font:inherit;text-align:left;font-size:.6875rem;padding:.0625rem .25rem;border-radius:.1875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem;color:var(--color-on-primary);cursor:pointer}.lc-calendar__event-dot--primary{background:var(--color-primary-500)}.lc-calendar__event-dot--secondary{background:var(--color-secondary-500)}.lc-calendar__event-dot--success{background:var(--color-success-default)}.lc-calendar__event-dot--warning{background:var(--color-warning-default)}.lc-calendar__event-dot--error{background:var(--color-error-default)}.lc-calendar__event-dot--info{background:var(--color-info-default)}.lc-calendar__more{font-size:.625rem;color:var(--color-text-tertiary);padding:.0625rem .25rem;cursor:pointer}.lc-calendar__time-grid{overflow:auto;max-height:40rem}.lc-calendar__time-header{display:flex;position:sticky;top:0;z-index:2;background:var(--color-surface-2);border-bottom:1px solid var(--color-border)}.lc-calendar__time-col-header{flex:1;min-width:0;text-align:center;padding:.5rem .25rem;border:none;border-left:1px solid var(--color-border);background:transparent;font:inherit;font-size:.75rem;font-weight:600;color:var(--color-text-secondary);cursor:pointer}.lc-calendar__time-col-header:hover{background:var(--color-surface-hover)}.lc-calendar__time-col-header--today{color:var(--color-primary-500);background:color-mix(in srgb,var(--color-primary-500) 6%,transparent)}.lc-calendar__time-body{display:flex;position:relative}.lc-calendar__time-gutter{width:3.5rem;flex-shrink:0;border-right:1px solid var(--color-border);background:var(--color-surface-2)}.lc-calendar__time-label{height:3.75rem;padding:.125rem .375rem 0 0;text-align:right;font-size:.6875rem;color:var(--color-text-tertiary)}.lc-calendar__time-col{flex:1;position:relative;border-left:1px solid var(--color-border)}.lc-calendar__time-col--single{min-width:0}.lc-calendar__time-slot{height:3.75rem;border-bottom:1px solid var(--color-divider);cursor:pointer}.lc-calendar__time-slot:hover{background:var(--color-surface-2)}.lc-calendar__time-event{position:absolute;left:.125rem;right:.125rem;border:none;border-radius:.25rem;padding:.125rem .375rem;font:inherit;font-size:.6875rem;text-align:left;color:var(--color-on-primary);overflow:hidden;cursor:pointer;z-index:1}.lc-calendar__time-event--primary{background:var(--color-primary-500)}.lc-calendar__time-event--secondary{background:var(--color-secondary-500)}.lc-calendar__time-event--success{background:var(--color-success-default)}.lc-calendar__time-event--warning{background:var(--color-warning-default)}.lc-calendar__time-event--error{background:var(--color-error-default)}.lc-calendar__time-event--info{background:var(--color-info-default)}\n"],
|
|
272
277
|
dependencies: [{
|
|
273
278
|
kind: "pipe",
|
|
@@ -289,7 +294,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
289
294
|
standalone: true,
|
|
290
295
|
imports: [SlicePipe],
|
|
291
296
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
292
|
-
template: "<div class=\"lc-calendar\">\n <!-- Header -->\n <div class=\"lc-calendar__header\">\n <div class=\"lc-calendar__nav\">\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigatePrev()\" [attr.aria-label]=\"text().previous\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10 12L6 8l4-4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <button type=\"button\" class=\"lc-calendar__today-btn\" (click)=\"navigateToday()\">{{ text().today }}</button>\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigateNext()\" [attr.aria-label]=\"text().next\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6 4l4 4-4 4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <span class=\"lc-calendar__title\" aria-live=\"polite\">{{ headerTitle() }}</span>\n </div>\n <div class=\"lc-calendar__view-toggle\" role=\"group\">\n <button type=\"button\" [class.active]=\"view() === 'day'\" [attr.aria-pressed]=\"view() === 'day'\" (click)=\"setView('day')\">{{ text().day }}</button>\n <button type=\"button\" [class.active]=\"view() === 'week'\" [attr.aria-pressed]=\"view() === 'week'\" (click)=\"setView('week')\">{{ text().week }}</button>\n <button type=\"button\" [class.active]=\"view() === 'month'\" [attr.aria-pressed]=\"view() === 'month'\" (click)=\"setView('month')\">{{ text().month }}</button>\n </div>\n </div>\n\n <!-- Month View -->\n @if (view() === 'month') {\n <div class=\"lc-calendar__month\">\n <div class=\"lc-calendar__weekdays\" aria-hidden=\"true\">\n @for (name of weekDayNames(); track name) {\n <div class=\"lc-calendar__weekday\">{{ name }}</div>\n }\n </div>\n @for (week of monthGrid(); track $index) {\n <div class=\"lc-calendar__week-row\">\n @for (cell of week; track cell.date.getTime()) {\n <!-- The cell stays a mouse target for the whole area; keyboard users reach the\n date through the day-number button and each event through its own button. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the whole cell; keyboard users use the day-number button -->\n <div\n class=\"lc-calendar__day-cell\"\n [class.lc-calendar__day-cell--today]=\"cell.isToday\"\n [class.lc-calendar__day-cell--other]=\"!cell.isCurrentMonth\"\n (click)=\"onDateClick(cell.date)\"\n >\n <button\n type=\"button\"\n class=\"lc-calendar__day-number\"\n [attr.aria-label]=\"dateLabel(cell.date)\"\n [attr.aria-current]=\"cell.isToday ? 'date' : null\"\n (click)=\"onDateClick(cell.date, $event)\"\n >\n {{ cell.date.getDate() }}\n </button>\n @for (evt of cell.events | slice:0:3; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__event-dot\"\n [class]=\"'lc-calendar__event-dot lc-calendar__event-dot--' + (evt.color || 'primary')\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n @if (cell.events.length > 3) {\n <div class=\"lc-calendar__more\">+{{ cell.events.length - 3 }}</div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Week View -->\n @if (view() === 'week') {\n <div class=\"lc-calendar__time-grid\">\n <div class=\"lc-calendar__time-header\">\n <div class=\"lc-calendar__time-gutter\"></div>\n @for (day of weekDays(); track day.date.getTime()) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-col-header\"\n [class.lc-calendar__time-col-header--today]=\"day.isToday\"\n [attr.aria-label]=\"
|
|
297
|
+
template: "<div class=\"lc-calendar\">\n <!-- Header -->\n <div class=\"lc-calendar__header\">\n <div class=\"lc-calendar__nav\">\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigatePrev()\" [attr.aria-label]=\"text().previous\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10 12L6 8l4-4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <button type=\"button\" class=\"lc-calendar__today-btn\" (click)=\"navigateToday()\">{{ text().today }}</button>\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigateNext()\" [attr.aria-label]=\"text().next\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6 4l4 4-4 4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <span class=\"lc-calendar__title\" aria-live=\"polite\">{{ headerTitle() }}</span>\n </div>\n <div class=\"lc-calendar__view-toggle\" role=\"group\">\n <button type=\"button\" [class.active]=\"view() === 'day'\" [attr.aria-pressed]=\"view() === 'day'\" (click)=\"setView('day')\">{{ text().day }}</button>\n <button type=\"button\" [class.active]=\"view() === 'week'\" [attr.aria-pressed]=\"view() === 'week'\" (click)=\"setView('week')\">{{ text().week }}</button>\n <button type=\"button\" [class.active]=\"view() === 'month'\" [attr.aria-pressed]=\"view() === 'month'\" (click)=\"setView('month')\">{{ text().month }}</button>\n </div>\n </div>\n\n <!-- Month View -->\n @if (view() === 'month') {\n <div class=\"lc-calendar__month\">\n <div class=\"lc-calendar__weekdays\" aria-hidden=\"true\">\n @for (name of weekDayNames(); track name) {\n <div class=\"lc-calendar__weekday\">{{ name }}</div>\n }\n </div>\n @for (week of monthGrid(); track $index) {\n <div class=\"lc-calendar__week-row\">\n @for (cell of week; track cell.date.getTime()) {\n <!-- The cell stays a mouse target for the whole area; keyboard users reach the\n date through the day-number button and each event through its own button. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the whole cell; keyboard users use the day-number button -->\n <div\n class=\"lc-calendar__day-cell\"\n [class.lc-calendar__day-cell--today]=\"cell.isToday\"\n [class.lc-calendar__day-cell--other]=\"!cell.isCurrentMonth\"\n (click)=\"onDateClick(cell.date)\"\n >\n <button\n type=\"button\"\n class=\"lc-calendar__day-number\"\n [attr.aria-label]=\"dateLabel(cell.date)\"\n [attr.aria-current]=\"cell.isToday ? 'date' : null\"\n (click)=\"onDateClick(cell.date, $event)\"\n >\n {{ cell.date.getDate() }}\n </button>\n @for (evt of cell.events | slice:0:3; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__event-dot\"\n [class]=\"'lc-calendar__event-dot lc-calendar__event-dot--' + (evt.color || 'primary')\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n @if (cell.events.length > 3) {\n <div class=\"lc-calendar__more\">+{{ cell.events.length - 3 }}</div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Week View -->\n @if (view() === 'week') {\n <div class=\"lc-calendar__time-grid\">\n <div class=\"lc-calendar__time-header\">\n <div class=\"lc-calendar__time-gutter\"></div>\n @for (day of weekDays(); track day.date.getTime()) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-col-header\"\n [class.lc-calendar__time-col-header--today]=\"day.isToday\"\n [attr.aria-label]=\"day.ariaLabel\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"onDateClick(day.date)\"\n >\n {{ day.label }}\n </button>\n }\n </div>\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n @for (day of weekDays(); track day.date.getTime()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col\" (click)=\"onDateClick(day.date)\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of day.events; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Day View -->\n @if (view() === 'day') {\n <div class=\"lc-calendar__time-grid lc-calendar__time-grid--day\">\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col lc-calendar__time-col--single\" (click)=\"onDateClick(currentDate())\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of dayEvents(); track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n </div>\n </div>\n }\n</div>\n",
|
|
293
298
|
styles: [".lc-calendar{font-family:var(--typography-font-family, sans-serif);border:1px solid var(--color-border);border-radius:.5rem;overflow:hidden;background:var(--color-surface)}.lc-calendar__header{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;border-bottom:1px solid var(--color-border);background:var(--color-surface-2)}.lc-calendar__nav{display:flex;align-items:center;gap:.5rem}.lc-calendar__time-event:focus-visible,.lc-calendar__time-col-header:focus-visible,.lc-calendar__event-dot:focus-visible,.lc-calendar__day-number:focus-visible,.lc-calendar__view-toggle button:focus-visible,.lc-calendar__today-btn:focus-visible,.lc-calendar__nav-btn:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:1px;z-index:2}.lc-calendar__nav-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--color-border);border-radius:.375rem;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;transition:background .15s,border-color .15s}.lc-calendar__nav-btn:hover{background:var(--color-surface-hover)}.lc-calendar__today-btn{padding:.25rem .75rem;border:1px solid var(--color-border);border-radius:.375rem;background:var(--color-surface);color:var(--color-text-secondary);font-size:.8125rem;font-weight:500;cursor:pointer;transition:background .15s}.lc-calendar__today-btn:hover{background:var(--color-surface-hover)}.lc-calendar__title{font-size:1rem;font-weight:600;color:var(--color-text-primary);margin-left:.25rem}.lc-calendar__view-toggle{display:flex;border:1px solid var(--color-border);border-radius:.375rem;overflow:hidden}.lc-calendar__view-toggle button{padding:.25rem .75rem;border:none;background:var(--color-surface);color:var(--color-text-secondary);font-size:.8125rem;font-weight:500;cursor:pointer;transition:background .15s,color .15s}.lc-calendar__view-toggle button:not(:last-child){border-right:1px solid var(--color-border)}.lc-calendar__view-toggle button:hover{background:var(--color-surface-2)}.lc-calendar__view-toggle button.active{background:var(--color-primary-500);color:var(--color-on-primary)}.lc-calendar__weekdays{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--color-border)}.lc-calendar__weekday{padding:.5rem;text-align:center;font-size:.75rem;font-weight:600;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.025em}.lc-calendar__week-row{display:grid;grid-template-columns:repeat(7,1fr)}.lc-calendar__week-row:not(:last-child){border-bottom:1px solid var(--color-divider)}.lc-calendar__day-cell{min-height:5.5rem;padding:.25rem;border-right:1px solid var(--color-divider);cursor:pointer;transition:background .1s}.lc-calendar__day-cell:last-child{border-right:none}.lc-calendar__day-cell:hover{background:var(--color-surface-2)}.lc-calendar__day-cell--today{background:color-mix(in srgb,var(--color-primary-500) 6%,transparent)}.lc-calendar__day-cell--today:hover{background:color-mix(in srgb,var(--color-primary-500) 12%,transparent)}.lc-calendar__day-cell--other{opacity:.4}.lc-calendar__day-number{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;padding:0;border:none;background:transparent;font:inherit;font-size:.75rem;font-weight:500;color:var(--color-text-secondary);border-radius:50%;cursor:pointer}.lc-calendar__day-number:hover{background:var(--color-surface-hover)}.lc-calendar__day-cell--today .lc-calendar__day-number{background:var(--color-primary-500);color:var(--color-on-primary);font-weight:700}.lc-calendar__event-dot{display:block;width:100%;border:none;font:inherit;text-align:left;font-size:.6875rem;padding:.0625rem .25rem;border-radius:.1875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem;color:var(--color-on-primary);cursor:pointer}.lc-calendar__event-dot--primary{background:var(--color-primary-500)}.lc-calendar__event-dot--secondary{background:var(--color-secondary-500)}.lc-calendar__event-dot--success{background:var(--color-success-default)}.lc-calendar__event-dot--warning{background:var(--color-warning-default)}.lc-calendar__event-dot--error{background:var(--color-error-default)}.lc-calendar__event-dot--info{background:var(--color-info-default)}.lc-calendar__more{font-size:.625rem;color:var(--color-text-tertiary);padding:.0625rem .25rem;cursor:pointer}.lc-calendar__time-grid{overflow:auto;max-height:40rem}.lc-calendar__time-header{display:flex;position:sticky;top:0;z-index:2;background:var(--color-surface-2);border-bottom:1px solid var(--color-border)}.lc-calendar__time-col-header{flex:1;min-width:0;text-align:center;padding:.5rem .25rem;border:none;border-left:1px solid var(--color-border);background:transparent;font:inherit;font-size:.75rem;font-weight:600;color:var(--color-text-secondary);cursor:pointer}.lc-calendar__time-col-header:hover{background:var(--color-surface-hover)}.lc-calendar__time-col-header--today{color:var(--color-primary-500);background:color-mix(in srgb,var(--color-primary-500) 6%,transparent)}.lc-calendar__time-body{display:flex;position:relative}.lc-calendar__time-gutter{width:3.5rem;flex-shrink:0;border-right:1px solid var(--color-border);background:var(--color-surface-2)}.lc-calendar__time-label{height:3.75rem;padding:.125rem .375rem 0 0;text-align:right;font-size:.6875rem;color:var(--color-text-tertiary)}.lc-calendar__time-col{flex:1;position:relative;border-left:1px solid var(--color-border)}.lc-calendar__time-col--single{min-width:0}.lc-calendar__time-slot{height:3.75rem;border-bottom:1px solid var(--color-divider);cursor:pointer}.lc-calendar__time-slot:hover{background:var(--color-surface-2)}.lc-calendar__time-event{position:absolute;left:.125rem;right:.125rem;border:none;border-radius:.25rem;padding:.125rem .375rem;font:inherit;font-size:.6875rem;text-align:left;color:var(--color-on-primary);overflow:hidden;cursor:pointer;z-index:1}.lc-calendar__time-event--primary{background:var(--color-primary-500)}.lc-calendar__time-event--secondary{background:var(--color-secondary-500)}.lc-calendar__time-event--success{background:var(--color-success-default)}.lc-calendar__time-event--warning{background:var(--color-warning-default)}.lc-calendar__time-event--error{background:var(--color-error-default)}.lc-calendar__time-event--info{background:var(--color-info-default)}\n"]
|
|
294
299
|
}]
|
|
295
300
|
}],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-calendar.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/calendar/calendar.component.ts","../../../../libs/angular-ui-kit/src/lib/calendar/calendar.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n model,\n output,\n computed,\n signal,\n} from '@angular/core';\nimport { SlicePipe } from '@angular/common';\n\nexport type CalendarView = 'day' | 'week' | 'month';\n\n/** UI strings of the calendar chrome; override individual keys via the `labels` input. */\nexport interface CalendarLabels {\n today: string;\n day: string;\n week: string;\n month: string;\n previous: string;\n next: string;\n}\n\nconst DEFAULT_LABELS: CalendarLabels = {\n today: 'Heute',\n day: 'Tag',\n week: 'Woche',\n month: 'Monat',\n previous: 'Zurück',\n next: 'Weiter',\n};\n\nexport interface CalendarEvent {\n id: string;\n title: string;\n start: Date | string;\n end: Date | string;\n color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';\n allDay?: boolean;\n}\n\ninterface DayCell {\n date: Date;\n isToday: boolean;\n isCurrentMonth: boolean;\n events: CalendarEvent[];\n}\n\ninterface HourSlot {\n hour: number;\n label: string;\n}\n\n@Component({\n selector: 'lc-calendar',\n standalone: true,\n imports: [SlicePipe],\n templateUrl: './calendar.component.html',\n styleUrls: ['./calendar.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Calendar component for date-based event display and navigation.\n *\n * Features:\n * - Day, week, and month view modes\n * - Event display with six color variants\n * - Navigation controls for previous/next/today\n * - Configurable first day of week and locale\n * - Time grid for day and week views with hourly slots\n * - Click handlers for events and date cells\n *\n * @example\n * ```html\n * <lc-calendar [events]=\"events\" view=\"month\" [firstDayOfWeek]=\"1\" />\n * ```\n */\nexport class CalendarComponent {\n /** Current view mode. Two-way bindable; the view toggle writes it and emits `viewChange`. */\n view = model<CalendarView>('month');\n\n /** Events to display. */\n events = input<CalendarEvent[]>([]);\n\n /** First day of week (0=Sunday, 1=Monday). */\n firstDayOfWeek = input<number>(1);\n\n /** Locale for date formatting. */\n locale = input<string>('de-DE');\n\n /** Overrides for the built-in UI strings (defaults are German, matching the default locale). */\n labels = input<Partial<CalendarLabels>>({});\n\n /** Start hour for day/week view. */\n startHour = input<number>(6);\n\n /** End hour for day/week view. */\n endHour = input<number>(22);\n\n /** Emits when an event is clicked. */\n eventClick = output<CalendarEvent>();\n\n /** Emits when a date cell is clicked. */\n dateClick = output<Date>();\n\n protected readonly currentDate = signal(new Date());\n\n protected readonly text = computed<CalendarLabels>(() => ({ ...DEFAULT_LABELS, ...this.labels() }));\n\n protected readonly headerTitle = computed(() => {\n const d = this.currentDate();\n const loc = this.locale();\n const v = this.view();\n if (v === 'month') {\n return d.toLocaleDateString(loc, { month: 'long', year: 'numeric' });\n }\n if (v === 'week') {\n const start = this.weekStart(d);\n const end = new Date(start);\n end.setDate(end.getDate() + 6);\n return `${start.toLocaleDateString(loc, { day: 'numeric', month: 'short' })} – ${end.toLocaleDateString(loc, { day: 'numeric', month: 'short', year: 'numeric' })}`;\n }\n return d.toLocaleDateString(loc, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });\n });\n\n protected readonly weekDayNames = computed(() => {\n const loc = this.locale();\n const fdow = this.firstDayOfWeek();\n return Array.from({ length: 7 }, (_, i) => {\n const d = new Date(2024, 0, fdow + i); // Jan 2024 starts on Monday\n return d.toLocaleDateString(loc, { weekday: 'short' });\n });\n });\n\n protected readonly monthGrid = computed(() => {\n const d = this.currentDate();\n const evts = this.normalizedEvents();\n const fdow = this.firstDayOfWeek();\n const year = d.getFullYear();\n const month = d.getMonth();\n const today = new Date();\n\n const firstOfMonth = new Date(year, month, 1);\n let startOffset = firstOfMonth.getDay() - fdow;\n if (startOffset < 0) startOffset += 7;\n\n const startDate = new Date(year, month, 1 - startOffset);\n const weeks: DayCell[][] = [];\n\n for (let w = 0; w < 6; w++) {\n const week: DayCell[] = [];\n for (let wd = 0; wd < 7; wd++) {\n const cellDate = new Date(startDate);\n cellDate.setDate(startDate.getDate() + w * 7 + wd);\n week.push({\n date: cellDate,\n isToday: this.isSameDay(cellDate, today),\n isCurrentMonth: cellDate.getMonth() === month,\n events: evts.filter(e => this.isEventOnDate(e, cellDate)),\n });\n }\n weeks.push(week);\n }\n return weeks;\n });\n\n protected readonly hourSlots = computed((): HourSlot[] => {\n const start = this.startHour();\n const end = this.endHour();\n return Array.from({ length: end - start }, (_, i) => ({\n hour: start + i,\n label: `${String(start + i).padStart(2, '0')}:00`,\n }));\n });\n\n protected readonly weekDays = computed(() => {\n const d = this.currentDate();\n const start = this.weekStart(d);\n const loc = this.locale();\n const today = new Date();\n const evts = this.normalizedEvents();\n\n return Array.from({ length: 7 }, (_, i) => {\n const day = new Date(start);\n day.setDate(start.getDate() + i);\n return {\n date: day,\n label: day.toLocaleDateString(loc, { weekday: 'short', day: 'numeric' }),\n isToday: this.isSameDay(day, today),\n events: evts.filter(e => this.isEventOnDate(e, day)),\n };\n });\n });\n\n protected readonly dayEvents = computed(() => {\n const d = this.currentDate();\n const evts = this.normalizedEvents();\n return evts.filter(e => this.isEventOnDate(e, d));\n });\n\n private readonly normalizedEvents = computed(() =>\n this.events().map(e => ({\n ...e,\n start: e.start instanceof Date ? e.start : new Date(e.start),\n end: e.end instanceof Date ? e.end : new Date(e.end),\n }))\n );\n\n protected navigatePrev(): void {\n this.navigate(-1);\n }\n\n protected navigateNext(): void {\n this.navigate(1);\n }\n\n protected navigateToday(): void {\n this.currentDate.set(new Date());\n }\n\n protected setView(v: CalendarView): void {\n this.view.set(v);\n }\n\n protected onEventClick(event: CalendarEvent, e: Event): void {\n // The event button sits inside a clickable cell/column; don't let it count as a date click too.\n e.stopPropagation();\n this.eventClick.emit(event);\n }\n\n protected onDateClick(date: Date, e?: Event): void {\n e?.stopPropagation();\n this.dateClick.emit(date);\n }\n\n /** Full, localized accessible name for a day button (e.g. \"Montag, 15. Januar 2024\"). */\n protected dateLabel(date: Date): string {\n return date.toLocaleDateString(this.locale(), {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n });\n }\n\n /** Accessible name for an event button: title plus its time range (or \"all day\"). */\n protected eventLabel(event: CalendarEvent): string {\n const start = event.start instanceof Date ? event.start : new Date(event.start);\n const end = event.end instanceof Date ? event.end : new Date(event.end);\n const loc = this.locale();\n const opts: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };\n return event.allDay\n ? event.title\n : `${event.title}, ${start.toLocaleTimeString(loc, opts)} – ${end.toLocaleTimeString(loc, opts)}`;\n }\n\n protected eventTop(event: CalendarEvent): number {\n const start = event.start instanceof Date ? event.start : new Date(event.start);\n const h = start.getHours() - this.startHour();\n const m = start.getMinutes();\n return (h * 60 + m);\n }\n\n protected eventHeight(event: CalendarEvent): number {\n const start = event.start instanceof Date ? event.start : new Date(event.start);\n const end = event.end instanceof Date ? event.end : new Date(event.end);\n const diff = (end.getTime() - start.getTime()) / 60000;\n return Math.max(diff, 15);\n }\n\n private navigate(direction: number): void {\n const d = new Date(this.currentDate());\n const v = this.view();\n if (v === 'month') {\n d.setMonth(d.getMonth() + direction);\n } else if (v === 'week') {\n d.setDate(d.getDate() + direction * 7);\n } else {\n d.setDate(d.getDate() + direction);\n }\n this.currentDate.set(d);\n }\n\n private weekStart(d: Date): Date {\n const start = new Date(d);\n const fdow = this.firstDayOfWeek();\n let diff = start.getDay() - fdow;\n if (diff < 0) diff += 7;\n start.setDate(start.getDate() - diff);\n return start;\n }\n\n private isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n }\n\n private isEventOnDate(event: { start: Date; end: Date }, date: Date): boolean {\n const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());\n const endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);\n return event.start <= endOfDay && event.end >= startOfDay;\n }\n}\n","<div class=\"lc-calendar\">\n <!-- Header -->\n <div class=\"lc-calendar__header\">\n <div class=\"lc-calendar__nav\">\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigatePrev()\" [attr.aria-label]=\"text().previous\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10 12L6 8l4-4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <button type=\"button\" class=\"lc-calendar__today-btn\" (click)=\"navigateToday()\">{{ text().today }}</button>\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigateNext()\" [attr.aria-label]=\"text().next\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6 4l4 4-4 4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <span class=\"lc-calendar__title\" aria-live=\"polite\">{{ headerTitle() }}</span>\n </div>\n <div class=\"lc-calendar__view-toggle\" role=\"group\">\n <button type=\"button\" [class.active]=\"view() === 'day'\" [attr.aria-pressed]=\"view() === 'day'\" (click)=\"setView('day')\">{{ text().day }}</button>\n <button type=\"button\" [class.active]=\"view() === 'week'\" [attr.aria-pressed]=\"view() === 'week'\" (click)=\"setView('week')\">{{ text().week }}</button>\n <button type=\"button\" [class.active]=\"view() === 'month'\" [attr.aria-pressed]=\"view() === 'month'\" (click)=\"setView('month')\">{{ text().month }}</button>\n </div>\n </div>\n\n <!-- Month View -->\n @if (view() === 'month') {\n <div class=\"lc-calendar__month\">\n <div class=\"lc-calendar__weekdays\" aria-hidden=\"true\">\n @for (name of weekDayNames(); track name) {\n <div class=\"lc-calendar__weekday\">{{ name }}</div>\n }\n </div>\n @for (week of monthGrid(); track $index) {\n <div class=\"lc-calendar__week-row\">\n @for (cell of week; track cell.date.getTime()) {\n <!-- The cell stays a mouse target for the whole area; keyboard users reach the\n date through the day-number button and each event through its own button. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the whole cell; keyboard users use the day-number button -->\n <div\n class=\"lc-calendar__day-cell\"\n [class.lc-calendar__day-cell--today]=\"cell.isToday\"\n [class.lc-calendar__day-cell--other]=\"!cell.isCurrentMonth\"\n (click)=\"onDateClick(cell.date)\"\n >\n <button\n type=\"button\"\n class=\"lc-calendar__day-number\"\n [attr.aria-label]=\"dateLabel(cell.date)\"\n [attr.aria-current]=\"cell.isToday ? 'date' : null\"\n (click)=\"onDateClick(cell.date, $event)\"\n >\n {{ cell.date.getDate() }}\n </button>\n @for (evt of cell.events | slice:0:3; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__event-dot\"\n [class]=\"'lc-calendar__event-dot lc-calendar__event-dot--' + (evt.color || 'primary')\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n @if (cell.events.length > 3) {\n <div class=\"lc-calendar__more\">+{{ cell.events.length - 3 }}</div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Week View -->\n @if (view() === 'week') {\n <div class=\"lc-calendar__time-grid\">\n <div class=\"lc-calendar__time-header\">\n <div class=\"lc-calendar__time-gutter\"></div>\n @for (day of weekDays(); track day.date.getTime()) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-col-header\"\n [class.lc-calendar__time-col-header--today]=\"day.isToday\"\n [attr.aria-label]=\"dateLabel(day.date)\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"onDateClick(day.date)\"\n >\n {{ day.label }}\n </button>\n }\n </div>\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n @for (day of weekDays(); track day.date.getTime()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col\" (click)=\"onDateClick(day.date)\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of day.events; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Day View -->\n @if (view() === 'day') {\n <div class=\"lc-calendar__time-grid lc-calendar__time-grid--day\">\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col lc-calendar__time-col--single\" (click)=\"onDateClick(currentDate())\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of dayEvents(); track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n </div>\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-calendar.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/calendar/calendar.component.ts","../../../../libs/angular-ui-kit/src/lib/calendar/calendar.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n model,\n output,\n computed,\n signal,\n} from '@angular/core';\nimport { SlicePipe } from '@angular/common';\n\nexport type CalendarView = 'day' | 'week' | 'month';\n\n/** UI strings of the calendar chrome; override individual keys via the `labels` input. */\nexport interface CalendarLabels {\n today: string;\n day: string;\n week: string;\n month: string;\n previous: string;\n next: string;\n}\n\nconst DEFAULT_LABELS: CalendarLabels = {\n today: 'Heute',\n day: 'Tag',\n week: 'Woche',\n month: 'Monat',\n previous: 'Zurück',\n next: 'Weiter',\n};\n\nexport interface CalendarEvent {\n id: string;\n title: string;\n start: Date | string;\n end: Date | string;\n color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';\n allDay?: boolean;\n}\n\ninterface DayCell {\n date: Date;\n isToday: boolean;\n isCurrentMonth: boolean;\n events: CalendarEvent[];\n}\n\ninterface HourSlot {\n hour: number;\n label: string;\n}\n\n@Component({\n selector: 'lc-calendar',\n standalone: true,\n imports: [SlicePipe],\n templateUrl: './calendar.component.html',\n styleUrls: ['./calendar.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Calendar component for date-based event display and navigation.\n *\n * Features:\n * - Day, week, and month view modes\n * - Event display with six color variants\n * - Navigation controls for previous/next/today\n * - Configurable first day of week and locale\n * - Time grid for day and week views with hourly slots\n * - Click handlers for events and date cells\n *\n * @example\n * ```html\n * <lc-calendar [events]=\"events\" view=\"month\" [firstDayOfWeek]=\"1\" />\n * ```\n */\nexport class CalendarComponent {\n /** Current view mode. Two-way bindable; the view toggle writes it and emits `viewChange`. */\n view = model<CalendarView>('month');\n\n /** Events to display. */\n events = input<CalendarEvent[]>([]);\n\n /** First day of week (0=Sunday, 1=Monday). */\n firstDayOfWeek = input<number>(1);\n\n /** Locale for date formatting. */\n locale = input<string>('de-DE');\n\n /** Overrides for the built-in UI strings (defaults are German, matching the default locale). */\n labels = input<Partial<CalendarLabels>>({});\n\n /** Start hour for day/week view. */\n startHour = input<number>(6);\n\n /** End hour for day/week view. */\n endHour = input<number>(22);\n\n /** Emits when an event is clicked. */\n eventClick = output<CalendarEvent>();\n\n /** Emits when a date cell is clicked. */\n dateClick = output<Date>();\n\n protected readonly currentDate = signal(new Date());\n\n protected readonly text = computed<CalendarLabels>(() => ({ ...DEFAULT_LABELS, ...this.labels() }));\n\n protected readonly headerTitle = computed(() => {\n const d = this.currentDate();\n const loc = this.locale();\n const v = this.view();\n if (v === 'month') {\n return d.toLocaleDateString(loc, { month: 'long', year: 'numeric' });\n }\n if (v === 'week') {\n const start = this.weekStart(d);\n const end = new Date(start);\n end.setDate(end.getDate() + 6);\n return `${start.toLocaleDateString(loc, { day: 'numeric', month: 'short' })} – ${end.toLocaleDateString(loc, { day: 'numeric', month: 'short', year: 'numeric' })}`;\n }\n return d.toLocaleDateString(loc, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });\n });\n\n protected readonly weekDayNames = computed(() => {\n const loc = this.locale();\n const fdow = this.firstDayOfWeek();\n return Array.from({ length: 7 }, (_, i) => {\n const d = new Date(2024, 0, fdow + i); // Jan 2024 starts on Monday\n return d.toLocaleDateString(loc, { weekday: 'short' });\n });\n });\n\n protected readonly monthGrid = computed(() => {\n const d = this.currentDate();\n const evts = this.normalizedEvents();\n const fdow = this.firstDayOfWeek();\n const year = d.getFullYear();\n const month = d.getMonth();\n const today = new Date();\n\n const firstOfMonth = new Date(year, month, 1);\n let startOffset = firstOfMonth.getDay() - fdow;\n if (startOffset < 0) startOffset += 7;\n\n const startDate = new Date(year, month, 1 - startOffset);\n const weeks: DayCell[][] = [];\n\n for (let w = 0; w < 6; w++) {\n const week: DayCell[] = [];\n for (let wd = 0; wd < 7; wd++) {\n const cellDate = new Date(startDate);\n cellDate.setDate(startDate.getDate() + w * 7 + wd);\n week.push({\n date: cellDate,\n isToday: this.isSameDay(cellDate, today),\n isCurrentMonth: cellDate.getMonth() === month,\n events: evts.filter(e => this.isEventOnDate(e, cellDate)),\n });\n }\n weeks.push(week);\n }\n return weeks;\n });\n\n protected readonly hourSlots = computed((): HourSlot[] => {\n const start = this.startHour();\n const end = this.endHour();\n return Array.from({ length: end - start }, (_, i) => ({\n hour: start + i,\n label: `${String(start + i).padStart(2, '0')}:00`,\n }));\n });\n\n protected readonly weekDays = computed(() => {\n const d = this.currentDate();\n const start = this.weekStart(d);\n const loc = this.locale();\n const today = new Date();\n const evts = this.normalizedEvents();\n\n return Array.from({ length: 7 }, (_, i) => {\n const day = new Date(start);\n day.setDate(start.getDate() + i);\n const label = day.toLocaleDateString(loc, { weekday: 'short', day: 'numeric' });\n return {\n date: day,\n label,\n // The visible text, then the day's own month and year (AS-33, WCAG\n // 2.5.3 Label in Name): \"Mo., 19. Oktober 2026\".\n ariaLabel: `${label} ${day.toLocaleDateString(loc, { month: 'long', year: 'numeric' })}`,\n isToday: this.isSameDay(day, today),\n events: evts.filter(e => this.isEventOnDate(e, day)),\n };\n });\n });\n\n protected readonly dayEvents = computed(() => {\n const d = this.currentDate();\n const evts = this.normalizedEvents();\n return evts.filter(e => this.isEventOnDate(e, d));\n });\n\n private readonly normalizedEvents = computed(() =>\n this.events().map(e => ({\n ...e,\n start: e.start instanceof Date ? e.start : new Date(e.start),\n end: e.end instanceof Date ? e.end : new Date(e.end),\n }))\n );\n\n protected navigatePrev(): void {\n this.navigate(-1);\n }\n\n protected navigateNext(): void {\n this.navigate(1);\n }\n\n protected navigateToday(): void {\n this.currentDate.set(new Date());\n }\n\n protected setView(v: CalendarView): void {\n this.view.set(v);\n }\n\n protected onEventClick(event: CalendarEvent, e: Event): void {\n // The event button sits inside a clickable cell/column; don't let it count as a date click too.\n e.stopPropagation();\n this.eventClick.emit(event);\n }\n\n protected onDateClick(date: Date, e?: Event): void {\n e?.stopPropagation();\n this.dateClick.emit(date);\n }\n\n /** Full, localized accessible name for a day button (e.g. \"Montag, 15. Januar 2024\"). */\n protected dateLabel(date: Date): string {\n return date.toLocaleDateString(this.locale(), {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n });\n }\n\n /** Accessible name for an event button: title plus its time range (or \"all day\"). */\n protected eventLabel(event: CalendarEvent): string {\n const start = event.start instanceof Date ? event.start : new Date(event.start);\n const end = event.end instanceof Date ? event.end : new Date(event.end);\n const loc = this.locale();\n const opts: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };\n return event.allDay\n ? event.title\n : `${event.title}, ${start.toLocaleTimeString(loc, opts)} – ${end.toLocaleTimeString(loc, opts)}`;\n }\n\n protected eventTop(event: CalendarEvent): number {\n const start = event.start instanceof Date ? event.start : new Date(event.start);\n const h = start.getHours() - this.startHour();\n const m = start.getMinutes();\n return (h * 60 + m);\n }\n\n protected eventHeight(event: CalendarEvent): number {\n const start = event.start instanceof Date ? event.start : new Date(event.start);\n const end = event.end instanceof Date ? event.end : new Date(event.end);\n const diff = (end.getTime() - start.getTime()) / 60000;\n return Math.max(diff, 15);\n }\n\n private navigate(direction: number): void {\n const d = new Date(this.currentDate());\n const v = this.view();\n if (v === 'month') {\n d.setMonth(d.getMonth() + direction);\n } else if (v === 'week') {\n d.setDate(d.getDate() + direction * 7);\n } else {\n d.setDate(d.getDate() + direction);\n }\n this.currentDate.set(d);\n }\n\n private weekStart(d: Date): Date {\n const start = new Date(d);\n const fdow = this.firstDayOfWeek();\n let diff = start.getDay() - fdow;\n if (diff < 0) diff += 7;\n start.setDate(start.getDate() - diff);\n return start;\n }\n\n private isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n }\n\n private isEventOnDate(event: { start: Date; end: Date }, date: Date): boolean {\n const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());\n const endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);\n return event.start <= endOfDay && event.end >= startOfDay;\n }\n}\n","<div class=\"lc-calendar\">\n <!-- Header -->\n <div class=\"lc-calendar__header\">\n <div class=\"lc-calendar__nav\">\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigatePrev()\" [attr.aria-label]=\"text().previous\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10 12L6 8l4-4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <button type=\"button\" class=\"lc-calendar__today-btn\" (click)=\"navigateToday()\">{{ text().today }}</button>\n <button type=\"button\" class=\"lc-calendar__nav-btn\" (click)=\"navigateNext()\" [attr.aria-label]=\"text().next\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6 4l4 4-4 4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n </button>\n <span class=\"lc-calendar__title\" aria-live=\"polite\">{{ headerTitle() }}</span>\n </div>\n <div class=\"lc-calendar__view-toggle\" role=\"group\">\n <button type=\"button\" [class.active]=\"view() === 'day'\" [attr.aria-pressed]=\"view() === 'day'\" (click)=\"setView('day')\">{{ text().day }}</button>\n <button type=\"button\" [class.active]=\"view() === 'week'\" [attr.aria-pressed]=\"view() === 'week'\" (click)=\"setView('week')\">{{ text().week }}</button>\n <button type=\"button\" [class.active]=\"view() === 'month'\" [attr.aria-pressed]=\"view() === 'month'\" (click)=\"setView('month')\">{{ text().month }}</button>\n </div>\n </div>\n\n <!-- Month View -->\n @if (view() === 'month') {\n <div class=\"lc-calendar__month\">\n <div class=\"lc-calendar__weekdays\" aria-hidden=\"true\">\n @for (name of weekDayNames(); track name) {\n <div class=\"lc-calendar__weekday\">{{ name }}</div>\n }\n </div>\n @for (week of monthGrid(); track $index) {\n <div class=\"lc-calendar__week-row\">\n @for (cell of week; track cell.date.getTime()) {\n <!-- The cell stays a mouse target for the whole area; keyboard users reach the\n date through the day-number button and each event through its own button. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the whole cell; keyboard users use the day-number button -->\n <div\n class=\"lc-calendar__day-cell\"\n [class.lc-calendar__day-cell--today]=\"cell.isToday\"\n [class.lc-calendar__day-cell--other]=\"!cell.isCurrentMonth\"\n (click)=\"onDateClick(cell.date)\"\n >\n <button\n type=\"button\"\n class=\"lc-calendar__day-number\"\n [attr.aria-label]=\"dateLabel(cell.date)\"\n [attr.aria-current]=\"cell.isToday ? 'date' : null\"\n (click)=\"onDateClick(cell.date, $event)\"\n >\n {{ cell.date.getDate() }}\n </button>\n @for (evt of cell.events | slice:0:3; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__event-dot\"\n [class]=\"'lc-calendar__event-dot lc-calendar__event-dot--' + (evt.color || 'primary')\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n @if (cell.events.length > 3) {\n <div class=\"lc-calendar__more\">+{{ cell.events.length - 3 }}</div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Week View -->\n @if (view() === 'week') {\n <div class=\"lc-calendar__time-grid\">\n <div class=\"lc-calendar__time-header\">\n <div class=\"lc-calendar__time-gutter\"></div>\n @for (day of weekDays(); track day.date.getTime()) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-col-header\"\n [class.lc-calendar__time-col-header--today]=\"day.isToday\"\n [attr.aria-label]=\"day.ariaLabel\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"onDateClick(day.date)\"\n >\n {{ day.label }}\n </button>\n }\n </div>\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n @for (day of weekDays(); track day.date.getTime()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col\" (click)=\"onDateClick(day.date)\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of day.events; track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Day View -->\n @if (view() === 'day') {\n <div class=\"lc-calendar__time-grid lc-calendar__time-grid--day\">\n <div class=\"lc-calendar__time-body\">\n <div class=\"lc-calendar__time-gutter\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-label\">{{ slot.label }}</div>\n }\n </div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- mouse target for the empty column; events and dates have their own buttons -->\n <div class=\"lc-calendar__time-col lc-calendar__time-col--single\" (click)=\"onDateClick(currentDate())\">\n @for (slot of hourSlots(); track slot.hour) {\n <div class=\"lc-calendar__time-slot\"></div>\n }\n @for (evt of dayEvents(); track evt.id) {\n <button\n type=\"button\"\n class=\"lc-calendar__time-event\"\n [class]=\"'lc-calendar__time-event lc-calendar__time-event--' + (evt.color || 'primary')\"\n [style.top.px]=\"eventTop(evt)\"\n [style.height.px]=\"eventHeight(evt)\"\n [attr.aria-label]=\"eventLabel(evt)\"\n [title]=\"evt.title\"\n (click)=\"onEventClick(evt, $event)\"\n >\n {{ evt.title }}\n </button>\n }\n </div>\n </div>\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
@@ -23,6 +23,11 @@ var FileUploadComponent = class FileUploadComponent {
|
|
|
23
23
|
this.disabled() ? "file-upload--disabled" : ""
|
|
24
24
|
].filter(Boolean).join(" ");
|
|
25
25
|
}, ...ngDevMode ? [{ debugName: "dropzoneClasses" }] : /* istanbul ignore next */ []);
|
|
26
|
+
dropzoneAriaLabel = computed(() => [
|
|
27
|
+
this.hint(),
|
|
28
|
+
this.accept() ? `${this.acceptedLabel()} ${this.accept()}` : "",
|
|
29
|
+
this.maxSizeBytes() > 0 ? `${this.maxSizeLabel()} ${this.formatSize(this.maxSizeBytes())}` : ""
|
|
30
|
+
].filter(Boolean).join(", "), ...ngDevMode ? [{ debugName: "dropzoneAriaLabel" }] : /* istanbul ignore next */ []);
|
|
26
31
|
onDragOver(event) {
|
|
27
32
|
event.preventDefault();
|
|
28
33
|
event.stopPropagation();
|
|
@@ -163,7 +168,7 @@ var FileUploadComponent = class FileUploadComponent {
|
|
|
163
168
|
isSignal: true
|
|
164
169
|
}],
|
|
165
170
|
ngImport: i0,
|
|
166
|
-
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]=\"
|
|
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",
|
|
167
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"],
|
|
168
173
|
dependencies: [{
|
|
169
174
|
kind: "component",
|
|
@@ -196,7 +201,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
196
201
|
imports: [IconComponent],
|
|
197
202
|
viewProviders: [ɵprovideKitIcons([ICON_FILE, ICON_X_MARK])],
|
|
198
203
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
199
|
-
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]=\"
|
|
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",
|
|
200
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"]
|
|
201
206
|
}]
|
|
202
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.\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 /** Prefix of the accepted file types line (`accept`). */\n acceptedLabel = input('Accepted:');\n\n /** Prefix of the maximum file size line (`maxSizeBytes`). */\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 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]=\"hint()\"\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 <p class=\"file-upload__hint\">{{ hint() }}</p>\n @if (accept()) {\n <p class=\"file-upload__accept\">{{ acceptedLabel() }} {{ accept() }}</p>\n }\n @if (maxSizeBytes() > 0) {\n <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\"></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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"}
|