@compsych-ui-components/angular 6.1.1 → 6.1.2

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/esm2022/index.js CHANGED
@@ -1,23 +1,25 @@
1
1
  import { NgModule } from '@angular/core';
2
2
  import { ButtonComponent } from './lib/button/button';
3
3
  import { AccordionComponent } from './lib/accordion/accordion';
4
+ import { FooterComponent } from './lib/footer/footer';
4
5
  import { HeaderNavComponent } from './lib/header-nav/header-nav';
5
6
  import { SidebarComponent } from './lib/sidebar/sidebar';
6
7
  import * as i0 from "@angular/core";
7
8
  export { ButtonComponent } from './lib/button/button';
8
9
  export { AccordionComponent } from './lib/accordion/accordion';
10
+ export { FooterComponent } from './lib/footer/footer';
9
11
  export { HeaderNavComponent } from './lib/header-nav/header-nav';
10
12
  export { SidebarComponent } from './lib/sidebar/sidebar';
11
13
  export class CompsychAngularModule {
12
14
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
13
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, imports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent], exports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent] });
15
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, imports: [ButtonComponent, AccordionComponent, FooterComponent, HeaderNavComponent, SidebarComponent], exports: [ButtonComponent, AccordionComponent, FooterComponent, HeaderNavComponent, SidebarComponent] });
14
16
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule });
15
17
  }
16
18
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, decorators: [{
17
19
  type: NgModule,
18
20
  args: [{
19
- imports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent],
20
- exports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent],
21
+ imports: [ButtonComponent, AccordionComponent, FooterComponent, HeaderNavComponent, SidebarComponent],
22
+ exports: [ButtonComponent, AccordionComponent, FooterComponent, HeaderNavComponent, SidebarComponent],
21
23
  }]
22
24
  }] });
23
25
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/angular/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAOzD,MAAM,OAAO,qBAAqB;wGAArB,qBAAqB;yGAArB,qBAAqB,YAHtB,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,aACzE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB;yGAExE,qBAAqB;;4FAArB,qBAAqB;kBAJjC,QAAQ;mBAAC;oBACR,OAAO,EAAE,CAAC,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,CAAC;oBACpF,OAAO,EAAE,CAAC,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,CAAC;iBACrF","sourcesContent":["import { NgModule } from '@angular/core';\r\nimport { ButtonComponent } from './lib/button/button';\r\nimport { AccordionComponent } from './lib/accordion/accordion';\r\nimport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nimport { SidebarComponent } from './lib/sidebar/sidebar';\r\n\r\nexport { ButtonComponent } from './lib/button/button';\r\nexport type { ButtonVariant, ButtonSize, ButtonIconPosition, ButtonType } from './lib/button/button';\r\nexport { AccordionComponent } from './lib/accordion/accordion';\r\nexport type { AccordionItem, AccordionButton, AccordionVariant, NestedAccordionItem } from './lib/accordion/accordion-types';\r\nexport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nexport type { HeaderNavLevel, NavAction } from './lib/header-nav/header-nav';\r\nexport { SidebarComponent } from './lib/sidebar/sidebar';\r\nexport type { SidebarItem } from './lib/sidebar/sidebar';\r\n\r\n@NgModule({\r\n imports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent],\r\n exports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent],\r\n})\r\nexport class CompsychAngularModule {}\r\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/angular/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAOzD,MAAM,OAAO,qBAAqB;wGAArB,qBAAqB;yGAArB,qBAAqB,YAHtB,eAAe,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,aAC1F,eAAe,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB;yGAEzF,qBAAqB;;4FAArB,qBAAqB;kBAJjC,QAAQ;mBAAC;oBACR,OAAO,EAAE,CAAC,eAAe,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,CAAC;oBACrG,OAAO,EAAE,CAAC,eAAe,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,CAAC;iBACtG","sourcesContent":["import { NgModule } from '@angular/core';\r\nimport { ButtonComponent } from './lib/button/button';\r\nimport { AccordionComponent } from './lib/accordion/accordion';\r\nimport { FooterComponent } from './lib/footer/footer';\r\nimport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nimport { SidebarComponent } from './lib/sidebar/sidebar';\r\n\r\nexport { ButtonComponent } from './lib/button/button';\r\nexport type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';\r\nexport { AccordionComponent } from './lib/accordion/accordion';\r\nexport type { AccordionItem, AccordionButton, AccordionVariant, NestedAccordionItem } from './lib/accordion/accordion-types';\r\nexport { FooterComponent } from './lib/footer/footer';\r\nexport type { FooterLink } from './lib/footer/footer';\r\nexport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nexport type { HeaderNavLevel, NavAction } from './lib/header-nav/header-nav';\r\nexport { SidebarComponent } from './lib/sidebar/sidebar';\r\nexport type { SidebarItem } from './lib/sidebar/sidebar';\r\n\r\n@NgModule({\r\n imports: [ButtonComponent, AccordionComponent, FooterComponent, HeaderNavComponent, SidebarComponent],\r\n exports: [ButtonComponent, AccordionComponent, FooterComponent, HeaderNavComponent, SidebarComponent],\r\n})\r\nexport class CompsychAngularModule {}\r\n"]}
@@ -119,7 +119,7 @@ export class AccordionItemComponent {
119
119
  </div>
120
120
  }
121
121
  </div>
122
- `, isInline: true, styles: [":host{display:block;border-bottom:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant)}:host:first-child{border-top:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant)}.trigger{display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);width:100%;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-24);background:none;border:none;cursor:pointer;text-align:left;color:var(--surface-surface-sys-on-surface);font-family:var(--font-core-title);font-size:var(--size-core-lg);font-weight:var(--label-large-sys-font-weight);line-height:var(--size-core-2xl);letter-spacing:var(--spacing-core-spacing-0);position:relative;overflow:hidden;border-radius:0}.trigger:before{content:\"\";position:absolute;inset:0;background-color:currentColor;opacity:0;transition:opacity .15s ease;pointer-events:none}.trigger:hover:before{opacity:.08}.trigger:active:before{opacity:.1}.trigger-label{flex:1;min-width:0}.trigger-leading-icon{display:flex;align-items:center;justify-content:center;width:var(--sys-size-sm);height:var(--sys-size-sm);color:var(--surface-surface-sys-on-surface);flex-shrink:0}.trigger-leading-icon svg{width:100%;height:100%}.trigger-toggle{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:1.5rem;height:1.5rem;font-size:1rem;line-height:1;color:var(--surface-surface-sys-on-surface-variant);background-color:var(--surface-surface-container-sys-surface-container);border-radius:var(--border-radius-sys-radius-full);box-sizing:border-box}.trigger[aria-expanded=true] .trigger-toggle{color:var(--add-on-primary-fixed-sys-on-primary-fixed);background-color:var(--add-on-primary-fixed-sys-primary-fixed-dim)}.actions{display:flex;flex-wrap:wrap;gap:var(--spacing-padding-sys-padding-12);align-items:center;justify-content:flex-end;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-24);border-top:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline)}:host(.level-2){border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:0;overflow:hidden}:host(.level-2):first-child{border-radius:var(--border-radius-sys-radius-sm) var(--border-radius-sys-radius-sm) 0 0}:host(.level-2):last-child{border-radius:0 0 var(--border-radius-sys-radius-sm) var(--border-radius-sys-radius-sm)}:host(.level-2):only-child{border-radius:var(--border-radius-sys-radius-sm)}:host(.level-2.nested){border-radius:var(--border-radius-sys-radius-sm)}:host(.level-2) .trigger{padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16);gap:var(--spacing-padding-sys-padding-4);font-size:var(--size-core-base);line-height:var(--size-core-xl)}:host(.level-2) .actions{padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24)}:host(.level-2) .content{padding:0 var(--spacing-padding-sys-padding-24) var(--spacing-padding-sys-padding-8)}:host(.text-only){border:none}:host(.text-only) .trigger{padding:var(--spacing-padding-sys-padding-8) 0;font-size:var(--size-core-base);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--size-core-xl)}:host(.text-only) .trigger-leading-icon{display:none}:host(.text-only) .trigger-toggle{order:-1;color:var(--surface-surface-sys-on-surface-variant);background-color:var(--surface-surface-container-sys-surface-container)}.content{padding:0 var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-16);color:var(--surface-surface-sys-on-surface-variant);font-family:var(--font-core-body);font-size:var(--size-core-base);font-weight:var(--label-large-sys-font-weight);line-height:var(--size-core-xl);letter-spacing:var(--spacing-core-spacing-0)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => ButtonComponent), selector: "compsych-button", inputs: ["label", "variant", "size", "icon", "iconPosition", "type", "ariaLabel", "iconOnly", "disabled", "loading"] }, { kind: "component", type: i0.forwardRef(() => AccordionComponent), selector: "compsych-accordion", inputs: ["items", "variant", "nested", "multi"] }] });
122
+ `, isInline: true, styles: [":host{display:block;border-bottom:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant)}:host:first-child{border-top:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant)}.trigger{display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);width:100%;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-24);background:none;border:none;cursor:pointer;text-align:left;color:var(--surface-surface-sys-on-surface);font-family:var(--font-core-title);font-size:var(--size-core-lg);font-weight:var(--label-large-sys-font-weight);line-height:var(--size-core-2xl);letter-spacing:var(--spacing-core-spacing-0);position:relative;overflow:hidden;border-radius:0}.trigger:before{content:\"\";position:absolute;inset:0;background-color:currentColor;opacity:0;transition:opacity .15s ease;pointer-events:none}.trigger:hover:before{opacity:.08}.trigger:active:before{opacity:.1}.trigger-label{flex:1;min-width:0}.trigger-leading-icon{display:flex;align-items:center;justify-content:center;width:var(--sys-size-sm);height:var(--sys-size-sm);color:var(--surface-surface-sys-on-surface);flex-shrink:0}.trigger-leading-icon svg{width:100%;height:100%}.trigger-toggle{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:1.5rem;height:1.5rem;font-size:1rem;line-height:1;color:var(--surface-surface-sys-on-surface-variant);background-color:var(--surface-surface-container-sys-surface-container);border-radius:var(--border-radius-sys-radius-full);box-sizing:border-box}.trigger[aria-expanded=true] .trigger-toggle{color:var(--add-on-primary-fixed-sys-on-primary-fixed);background-color:var(--add-on-primary-fixed-sys-primary-fixed-dim)}.actions{display:flex;flex-wrap:wrap;gap:var(--spacing-padding-sys-padding-12);align-items:center;justify-content:flex-end;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-24);border-top:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline)}:host(.level-2){border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:0;overflow:hidden}:host(.level-2):first-child{border-radius:var(--border-radius-sys-radius-sm) var(--border-radius-sys-radius-sm) 0 0}:host(.level-2):last-child{border-radius:0 0 var(--border-radius-sys-radius-sm) var(--border-radius-sys-radius-sm)}:host(.level-2):only-child{border-radius:var(--border-radius-sys-radius-sm)}:host(.level-2.nested){border-radius:var(--border-radius-sys-radius-sm)}:host(.level-2) .trigger{padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16);gap:var(--spacing-padding-sys-padding-4);font-size:var(--size-core-base);line-height:var(--size-core-xl)}:host(.level-2) .actions{padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24)}:host(.level-2) .content{padding:0 var(--spacing-padding-sys-padding-24) var(--spacing-padding-sys-padding-8)}:host(.text-only){border:none}:host(.text-only) .trigger{padding:var(--spacing-padding-sys-padding-8) 0;font-size:var(--size-core-base);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--size-core-xl)}:host(.text-only) .trigger-leading-icon{display:none}:host(.text-only) .trigger-toggle{order:-1;color:var(--surface-surface-sys-on-surface-variant);background-color:var(--surface-surface-container-sys-surface-container)}.content{padding:0 var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-16);color:var(--surface-surface-sys-on-surface-variant);font-family:var(--font-core-body);font-size:var(--size-core-base);font-weight:var(--label-large-sys-font-weight);line-height:var(--size-core-xl);letter-spacing:var(--spacing-core-spacing-0)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => ButtonComponent), selector: "compsych-button", inputs: ["label", "variant", "size", "leadingIcon", "trailingIcon", "type", "ariaLabel", "iconOnly", "disabled", "loading", "fullWidth"] }, { kind: "component", type: i0.forwardRef(() => AccordionComponent), selector: "compsych-accordion", inputs: ["items", "variant", "nested", "multi"] }] });
123
123
  }
124
124
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AccordionItemComponent, decorators: [{
125
125
  type: Component,
@@ -1 +1 @@
1
- {"version":3,"file":"accordion.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/accordion/accordion.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n Component,\r\n ElementRef,\r\n EventEmitter,\r\n forwardRef,\r\n HostListener,\r\n Input,\r\n Output,\r\n SecurityContext,\r\n} from '@angular/core';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\nimport { ButtonComponent } from '../button/button';\r\nimport { AccordionButton, AccordionItem, AccordionVariant, NestedAccordionItem } from './accordion-types';\r\n\r\nexport type { AccordionButton, AccordionItem, AccordionVariant, NestedAccordionItem };\r\n\r\nlet nextId = 0;\r\n\r\n// AccordionItemComponent is defined before AccordionComponent because AccordionComponent\r\n// uses it directly in its imports. AccordionItemComponent references AccordionComponent\r\n// via forwardRef to break the forward-reference cycle within this file.\r\n@Component({\r\n selector: 'compsych-accordion-item',\r\n standalone: true,\r\n imports: [ButtonComponent, forwardRef(() => AccordionComponent)],\r\n template: `\r\n <button\r\n class=\"trigger\"\r\n type=\"button\"\r\n [id]=\"triggerId\"\r\n [attr.aria-expanded]=\"expanded\"\r\n [attr.aria-controls]=\"panelId\"\r\n (click)=\"toggle()\"\r\n >\r\n @if (iconSvg) {\r\n <span class=\"trigger-leading-icon\" [innerHTML]=\"iconSvg\" aria-hidden=\"true\"></span>\r\n }\r\n <span class=\"trigger-label\">{{ title }}</span>\r\n <span class=\"trigger-toggle material-symbols-rounded\" aria-hidden=\"true\">{{ expanded ? 'remove' : 'add' }}</span>\r\n </button>\r\n <div\r\n [id]=\"panelId\"\r\n role=\"region\"\r\n [attr.aria-labelledby]=\"triggerId\"\r\n [hidden]=\"!expanded\"\r\n >\r\n @if (content) {\r\n <div class=\"content\" [innerHTML]=\"sanitizedContent\"></div>\r\n }\r\n @if (nestedContent.length) {\r\n <compsych-accordion [items]=\"nestedContent\" variant=\"level-2\" [nested]=\"true\" />\r\n }\r\n @if (buttons.length) {\r\n <div class=\"actions\">\r\n @for (btn of buttons; track btn.label) {\r\n <compsych-button [label]=\"btn.label\" variant=\"filled\" size=\"small\" (click)=\"btn.action()\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n `,\r\n styleUrl: './accordion-item.css',\r\n host: {\r\n class: 'compsych-accordion-item',\r\n '[class.level-2]': 'variant === \"level-2\"',\r\n '[class.text-only]': 'variant === \"text-only\"',\r\n '[class.nested]': 'nested',\r\n },\r\n})\r\n/**\r\n * A single expandable row used internally by `AccordionComponent`.\r\n * Consumers should use `compsych-accordion` with an `items` array rather\r\n * than placing `compsych-accordion-item` elements directly.\r\n */\r\nexport class AccordionItemComponent {\r\n readonly triggerId = `accordion-trigger-${nextId}`;\r\n readonly panelId = `accordion-panel-${nextId++}`;\r\n\r\n /** Text shown in the trigger button header. */\r\n @Input() title = '';\r\n /** HTML string rendered in the panel body when expanded. Sanitized before display. */\r\n @Input() content = '';\r\n /** Action buttons rendered at the bottom of the panel. */\r\n @Input() buttons: AccordionButton[] = [];\r\n /** Items rendered as a nested level-2 accordion inside this panel. */\r\n @Input() nestedContent: NestedAccordionItem[] = [];\r\n /** Visual style — inherited from the parent AccordionComponent. */\r\n @Input() variant: AccordionVariant = 'default';\r\n /** True when this item is rendered inside a parent accordion panel. Adds nested styling. Set automatically by AccordionComponent — do not set directly. */\r\n @Input() nested = false;\r\n\r\n /** Lucide icon name (kebab-case) shown to the left of the title. Pass an empty string to show no icon. */\r\n @Input()\r\n set icon(name: string) {\r\n const svg = LUCIDE_ICONS[name];\r\n this.iconSvg = svg ? this.sanitizer.bypassSecurityTrustHtml(svg) : null;\r\n }\r\n\r\n iconSvg: SafeHtml | null = null;\r\n\r\n get sanitizedContent(): SafeHtml {\r\n return this.sanitizer.sanitize(SecurityContext.HTML, this.content) ?? '';\r\n }\r\n\r\n constructor(\r\n private readonly sanitizer: DomSanitizer,\r\n private readonly el: ElementRef<HTMLElement>,\r\n ) {}\r\n\r\n /** Whether this panel is currently open. Supports boolean coercion (e.g. `[expanded]=\"true\"`). */\r\n @Input()\r\n get expanded(): boolean { return this._expanded; }\r\n set expanded(value: BooleanInput) { this._expanded = coerceBooleanProperty(value); }\r\n private _expanded = false;\r\n\r\n /** Emitted when the panel is toggled. `true` = opening, `false` = closing. */\r\n @Output() expandedChange = new EventEmitter<boolean>();\r\n\r\n toggle(): void {\r\n this._expanded = !this._expanded;\r\n this.expandedChange.emit(this._expanded);\r\n }\r\n\r\n @HostListener('keydown', ['$event'])\r\n onKeydown(event: KeyboardEvent): void {\r\n const target = event.target as HTMLElement;\r\n if (!(target instanceof HTMLButtonElement) || !target.classList.contains('trigger')) return;\r\n\r\n const parent = this.el.nativeElement.parentElement;\r\n if (!parent) return;\r\n\r\n const triggers = Array.from(\r\n parent.querySelectorAll<HTMLButtonElement>(':scope > compsych-accordion-item > button.trigger'),\r\n );\r\n const currentIndex = triggers.indexOf(target);\r\n let focusIndex = -1;\r\n\r\n switch (event.key) {\r\n case 'ArrowDown': focusIndex = currentIndex + 1; break;\r\n case 'ArrowUp': focusIndex = currentIndex - 1; break;\r\n case 'Home': focusIndex = 0; break;\r\n case 'End': focusIndex = triggers.length - 1; break;\r\n default: return;\r\n }\r\n\r\n if (focusIndex >= 0 && focusIndex < triggers.length) {\r\n triggers[focusIndex].focus();\r\n event.preventDefault();\r\n }\r\n }\r\n}\r\n\r\n@Component({\r\n selector: 'compsych-accordion',\r\n standalone: true,\r\n imports: [AccordionItemComponent],\r\n template: `\r\n @for (item of items; track $index; let i = $index) {\r\n <compsych-accordion-item\r\n [title]=\"item.title\"\r\n [content]=\"item.content ?? ''\"\r\n [icon]=\"item.icon ?? ''\"\r\n [buttons]=\"item.buttons ?? []\"\r\n [nestedContent]=\"item.nestedContent ?? []\"\r\n [variant]=\"variant\"\r\n [nested]=\"nested\"\r\n [expanded]=\"expandedSet.has(i)\"\r\n (expandedChange)=\"onExpandedChange(i, $event)\"\r\n />\r\n }\r\n `,\r\n styleUrl: './accordion.css',\r\n host: {\r\n '[class.level-2]': 'variant === \"level-2\"',\r\n '[class.text-only]': 'variant === \"text-only\"',\r\n '[class.nested]': 'nested',\r\n },\r\n})\r\nexport class AccordionComponent {\r\n /** Items to render as expandable rows. */\r\n @Input() items: AccordionItem[] = [];\r\n /** Visual style applied to all items. `'default'` is the standard card style; `'level-2'` is compact nested style; `'text-only'` removes card chrome. */\r\n @Input() variant: AccordionVariant = 'default';\r\n /** Set automatically when this accordion is rendered inside another accordion panel. Do not set directly. */\r\n @Input() nested = false;\r\n /** When true, multiple panels can be open simultaneously. Defaults to false — opening one panel closes any others. Supports boolean coercion. */\r\n @Input()\r\n get multi(): boolean { return this._multi; }\r\n set multi(value: BooleanInput) { this._multi = coerceBooleanProperty(value); }\r\n private _multi = false;\r\n\r\n expandedSet = new Set<number>();\r\n\r\n onExpandedChange(index: number, isExpanded: boolean): void {\r\n if (isExpanded) {\r\n if (!this._multi) this.expandedSet.clear();\r\n this.expandedSet.add(index);\r\n } else {\r\n this.expandedSet.delete(index);\r\n }\r\n this.expandedSet = new Set(this.expandedSet);\r\n }\r\n}\r\n"]}
1
+ {"version":3,"file":"accordion.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/accordion/accordion.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n Component,\r\n ElementRef,\r\n EventEmitter,\r\n forwardRef,\r\n HostListener,\r\n Input,\r\n Output,\r\n SecurityContext,\r\n} from '@angular/core';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\nimport { ButtonComponent } from '../button/button';\r\nimport { AccordionButton, AccordionItem, AccordionVariant, NestedAccordionItem } from './accordion-types';\r\n\r\nexport type { AccordionButton, AccordionItem, AccordionVariant, NestedAccordionItem };\r\n\r\nlet nextId = 0;\r\n\r\n// AccordionItemComponent is defined before AccordionComponent because AccordionComponent\r\n// uses it directly in its imports. AccordionItemComponent references AccordionComponent\r\n// via forwardRef to break the forward-reference cycle within this file.\r\n@Component({\r\n selector: 'compsych-accordion-item',\r\n standalone: true,\r\n imports: [ButtonComponent, forwardRef(() => AccordionComponent)],\r\n template: `\r\n <button\r\n class=\"trigger\"\r\n type=\"button\"\r\n [id]=\"triggerId\"\r\n [attr.aria-expanded]=\"expanded\"\r\n [attr.aria-controls]=\"panelId\"\r\n (click)=\"toggle()\"\r\n >\r\n @if (iconSvg) {\r\n <span class=\"trigger-leading-icon\" [innerHTML]=\"iconSvg\" aria-hidden=\"true\"></span>\r\n }\r\n <span class=\"trigger-label\">{{ title }}</span>\r\n <span class=\"trigger-toggle material-symbols-rounded\" aria-hidden=\"true\">{{ expanded ? 'remove' : 'add' }}</span>\r\n </button>\r\n <div\r\n [id]=\"panelId\"\r\n role=\"region\"\r\n [attr.aria-labelledby]=\"triggerId\"\r\n [hidden]=\"!expanded\"\r\n >\r\n @if (content) {\r\n <div class=\"content\" [innerHTML]=\"sanitizedContent\"></div>\r\n }\r\n @if (nestedContent.length) {\r\n <compsych-accordion [items]=\"nestedContent\" variant=\"level-2\" [nested]=\"true\" />\r\n }\r\n @if (buttons.length) {\r\n <div class=\"actions\">\r\n @for (btn of buttons; track btn.label) {\r\n <compsych-button [label]=\"btn.label\" variant=\"filled\" size=\"small\" (click)=\"btn.action()\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n `,\r\n styleUrl: './accordion-item.css',\r\n host: {\r\n class: 'compsych-accordion-item',\r\n '[class.level-2]': 'variant === \"level-2\"',\r\n '[class.text-only]': 'variant === \"text-only\"',\r\n '[class.nested]': 'nested',\r\n },\r\n})\r\n/**\r\n * A single expandable row used internally by `AccordionComponent`.\r\n * Consumers should use `compsych-accordion` with an `items` array rather\r\n * than placing `compsych-accordion-item` elements directly.\r\n */\r\nexport class AccordionItemComponent {\r\n readonly triggerId = `accordion-trigger-${nextId}`;\r\n readonly panelId = `accordion-panel-${nextId++}`;\r\n\r\n /** Text shown in the trigger button header. */\r\n @Input() title = '';\r\n /** HTML string rendered in the panel body when expanded. Sanitized before display. */\r\n @Input() content = '';\r\n /** Action buttons rendered at the bottom of the panel. */\r\n @Input() buttons: AccordionButton[] = [];\r\n /** Items rendered as a nested level-2 accordion inside this panel. */\r\n @Input() nestedContent: NestedAccordionItem[] = [];\r\n /** Visual style — inherited from the parent AccordionComponent. */\r\n @Input() variant: AccordionVariant = 'default';\r\n /** True when this item is rendered inside a parent accordion panel. Adds nested styling. Set automatically by AccordionComponent — do not set directly. */\r\n @Input() nested = false;\r\n\r\n /** Lucide icon name (kebab-case) shown to the left of the title. Pass an empty string to show no icon. */\r\n @Input()\r\n set icon(name: string) {\r\n const svg = LUCIDE_ICONS[name];\r\n this.iconSvg = svg ? this.sanitizer.bypassSecurityTrustHtml(svg) : null;\r\n }\r\n\r\n iconSvg: SafeHtml | null = null;\r\n\r\n get sanitizedContent(): SafeHtml {\r\n return this.sanitizer.sanitize(SecurityContext.HTML, this.content) ?? '';\r\n }\r\n\r\n constructor(\r\n private readonly sanitizer: DomSanitizer,\r\n private readonly el: ElementRef<HTMLElement>,\r\n ) {}\r\n\r\n /** Whether this panel is currently open. Supports boolean coercion (e.g. `[expanded]=\"true\"`). */\r\n @Input()\r\n get expanded(): boolean { return this._expanded; }\r\n set expanded(value: BooleanInput) { this._expanded = coerceBooleanProperty(value); }\r\n private _expanded = false;\r\n\r\n /** Emitted when the panel is toggled. `true` = opening, `false` = closing. */\r\n @Output() expandedChange = new EventEmitter<boolean>();\r\n\r\n toggle(): void {\r\n this._expanded = !this._expanded;\r\n this.expandedChange.emit(this._expanded);\r\n }\r\n\r\n @HostListener('keydown', ['$event'])\r\n onKeydown(event: KeyboardEvent): void {\r\n const target = event.target as HTMLElement;\r\n if (!(target instanceof HTMLButtonElement) || !target.classList.contains('trigger')) return;\r\n\r\n const parent = this.el.nativeElement.parentElement;\r\n if (!parent) return;\r\n\r\n const triggers = Array.from(\r\n parent.querySelectorAll<HTMLButtonElement>(':scope > compsych-accordion-item > button.trigger'),\r\n );\r\n const currentIndex = triggers.indexOf(target);\r\n let focusIndex = -1;\r\n\r\n switch (event.key) {\r\n case 'ArrowDown': focusIndex = currentIndex + 1; break;\r\n case 'ArrowUp': focusIndex = currentIndex - 1; break;\r\n case 'Home': focusIndex = 0; break;\r\n case 'End': focusIndex = triggers.length - 1; break;\r\n default: return;\r\n }\r\n\r\n if (focusIndex >= 0 && focusIndex < triggers.length) {\r\n triggers[focusIndex].focus();\r\n event.preventDefault();\r\n }\r\n }\r\n}\r\n\r\n@Component({\r\n selector: 'compsych-accordion',\r\n standalone: true,\r\n imports: [AccordionItemComponent],\r\n template: `\r\n @for (item of items; track $index; let i = $index) {\r\n <compsych-accordion-item\r\n [title]=\"item.title\"\r\n [content]=\"item.content ?? ''\"\r\n [icon]=\"item.icon ?? ''\"\r\n [buttons]=\"item.buttons ?? []\"\r\n [nestedContent]=\"item.nestedContent ?? []\"\r\n [variant]=\"variant\"\r\n [nested]=\"nested\"\r\n [expanded]=\"expandedSet.has(i)\"\r\n (expandedChange)=\"onExpandedChange(i, $event)\"\r\n />\r\n }\r\n `,\r\n styleUrl: './accordion.css',\r\n host: {\r\n '[class.level-2]': 'variant === \"level-2\"',\r\n '[class.text-only]': 'variant === \"text-only\"',\r\n '[class.nested]': 'nested',\r\n },\r\n})\r\nexport class AccordionComponent {\r\n /** Items to render as expandable rows. */\r\n @Input() items: AccordionItem[] = [];\r\n /** Visual style applied to all items. `'default'` is the standard card style; `'level-2'` is compact nested style; `'text-only'` removes card chrome. */\r\n @Input() variant: AccordionVariant = 'default';\r\n /** Set automatically when this accordion is rendered inside another accordion panel. Do not set directly. */\r\n @Input() nested = false;\r\n /** When true, multiple panels can be open simultaneously. Defaults to false — opening one panel closes any others. Supports boolean coercion. */\r\n @Input()\r\n get multi(): boolean { return this._multi; }\r\n set multi(value: BooleanInput) { this._multi = coerceBooleanProperty(value); }\r\n private _multi = false;\r\n\r\n expandedSet = new Set<number>();\r\n\r\n onExpandedChange(index: number, isExpanded: boolean): void {\r\n if (isExpanded) {\r\n if (!this._multi) this.expandedSet.clear();\r\n this.expandedSet.add(index);\r\n } else {\r\n this.expandedSet.delete(index);\r\n }\r\n this.expandedSet = new Set(this.expandedSet);\r\n }\r\n}\r\n"]}
@@ -1,38 +1,62 @@
1
- import { Component, Input, booleanAttribute } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, HostBinding, Input, booleanAttribute, inject, } from '@angular/core';
2
+ import { DomSanitizer } from '@angular/platform-browser';
3
+ import { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';
2
4
  import * as i0 from "@angular/core";
5
+ /** Icon pixel size per button size (per design: sm/md 16, lg 20, xl 24). */
6
+ const ICON_SIZE_BY_BUTTON_SIZE = {
7
+ small: 16,
8
+ medium: 16,
9
+ large: 20,
10
+ xlarge: 24,
11
+ };
3
12
  /**
4
13
  * A themeable button that renders a native `<button>` element, so it is fully
5
- * form-submittable and keyboard/screen-reader accessible out of the box.
14
+ * form-submittable and keyboard/screen-reader accessible out of the box. Icons
15
+ * are Lucide glyphs (matching the rest of the library); a button may carry a
16
+ * leading icon, a trailing icon, or both.
6
17
  */
7
18
  export class ButtonComponent {
19
+ sanitizer = inject(DomSanitizer);
8
20
  /** Text label rendered on the button. */
9
21
  label = '';
10
22
  /** Visual style of the button. Defaults to `'filled'`. */
11
23
  variant = 'filled';
12
- /** Button dimensions — controls height, padding, and font size. Defaults to `'small'`. */
24
+ /** Button dimensions — controls height, padding, font size, and icon size. Defaults to `'small'`. */
13
25
  size = 'small';
14
- /** Material Symbols icon name (e.g. `'search'`, `'arrow_forward'`). Omit to show label only. */
15
- icon;
16
- /** Placement of `icon` relative to the label. Defaults to `'leading'`. */
17
- iconPosition = 'leading';
26
+ /** Lucide icon name shown before the label (e.g. `'house'`). Swapped for a spinner when loading. */
27
+ leadingIcon;
28
+ /** Lucide icon name shown after the label (e.g. `'chevron-right'`). Can be combined with `leadingIcon`. */
29
+ trailingIcon;
18
30
  /** Native button type. Use `'submit'` inside a `<form>` to submit it. Defaults to `'button'`. */
19
31
  type = 'button';
20
32
  /**
21
- * Accessible name. Required for icon-only buttons since the icon is decorative
22
- * (`aria-hidden`). When `iconOnly` is set and this is omitted, `label` is used.
33
+ * Accessible name. Required for icon-only buttons since the icon is decorative.
34
+ * When `iconOnly` or `loading` is set and this is omitted, `label` is used.
23
35
  */
24
36
  ariaLabel;
25
- /** Hides the label and renders a square icon-only button. Requires `icon`. Supports boolean coercion. */
37
+ /** Hides the label and renders a square icon-only button. Uses `leadingIcon`. Supports boolean coercion. */
26
38
  iconOnly = false;
27
- /** Disables the button — non-interactive and dimmed. Supports boolean coercion. */
39
+ /** Disables the button — non-interactive and rendered in a neutral grey. Supports boolean coercion. */
28
40
  disabled = false;
29
41
  /**
30
- * Loading state. Replaces the content with a centered spinner while preserving
31
- * the button's width, blocks re-clicks, and exposes `aria-busy="true"`. The
32
- * variant's colours are retained (unlike `disabled`, which dims the button).
33
- * Supports boolean coercion.
42
+ * Loading state. Swaps the leading icon for a spinner and suppresses activation
43
+ * (native `disabled` blocks click/keyboard), while keeping the label and exposing
44
+ * `aria-busy="true"`. Retains the variant's colours. Supports boolean coercion.
34
45
  */
35
46
  loading = false;
47
+ /** Stretches the button to the full width of its container. Supports boolean coercion. */
48
+ fullWidth = false;
49
+ /** Mirrors `fullWidth` onto the host so it can stretch. */
50
+ get hostFullWidth() {
51
+ return this.fullWidth;
52
+ }
53
+ /** Pre-sanitized icon markup, recomputed on input change (avoids re-sanitizing every CD cycle). */
54
+ leadingIconSvg = null;
55
+ trailingIconSvg = null;
56
+ ngOnChanges() {
57
+ this.leadingIconSvg = this.leadingIcon ? this.buildIconSvg(this.leadingIcon) : null;
58
+ this.trailingIconSvg = this.trailingIcon ? this.buildIconSvg(this.trailingIcon) : null;
59
+ }
36
60
  /** Space-separated class list applied to the inner native button. */
37
61
  get buttonClasses() {
38
62
  return [
@@ -41,80 +65,149 @@ export class ButtonComponent {
41
65
  this.size,
42
66
  this.iconOnly ? 'icon-only' : '',
43
67
  this.loading ? 'loading' : '',
68
+ this.fullWidth ? 'full-width' : '',
44
69
  ]
45
70
  .filter(Boolean)
46
71
  .join(' ');
47
72
  }
48
- /** Resolves the accessible name, guaranteeing icon-only and loading buttons are labelled. */
73
+ /** Resolves the accessible name. Icon-only buttons have no visible text, so they fall back to `label`. */
49
74
  get computedAriaLabel() {
50
75
  if (this.ariaLabel) {
51
76
  return this.ariaLabel;
52
77
  }
53
- // Icon-only buttons have no visible text; while loading the label is hidden from the
54
- // a11y tree. In both cases fall back to `label` so the button keeps an accessible name.
55
- return this.iconOnly || this.loading ? this.label || null : null;
78
+ // Loading keeps the visible label, so only icon-only needs an explicit name here.
79
+ return this.iconOnly ? this.label || null : null;
80
+ }
81
+ /**
82
+ * Looks up a Lucide icon by name and injects an explicit width/height so the SVG
83
+ * renders at the correct size when injected via `[innerHTML]` (Angular's CSS
84
+ * encapsulation does not reach dynamically injected content).
85
+ */
86
+ buildIconSvg(name) {
87
+ const size = ICON_SIZE_BY_BUTTON_SIZE[this.size];
88
+ const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', `<svg width="${size}" height="${size}" `);
89
+ return this.sanitizer.bypassSecurityTrustHtml(svg);
90
+ }
91
+ /** M3 pressed ripple: pointer-down spawns a ripple at the pointer position. */
92
+ onPointerDown(event) {
93
+ if (this.disabled || this.loading || event.button !== 0) {
94
+ return;
95
+ }
96
+ const host = event.currentTarget;
97
+ const rect = host.getBoundingClientRect();
98
+ const x = event.clientX - rect.left;
99
+ const y = event.clientY - rect.top;
100
+ this.spawnRipple(host, x, y, this.farthestCornerDiameter(rect, x, y));
101
+ }
102
+ /** M3 pressed ripple: keyboard activation (Enter/Space) spawns a centered ripple. */
103
+ onKeyDown(event) {
104
+ if (this.disabled || this.loading || event.repeat) {
105
+ return;
106
+ }
107
+ if (event.key !== 'Enter' && event.key !== ' ') {
108
+ return;
109
+ }
110
+ const host = event.currentTarget;
111
+ const rect = host.getBoundingClientRect();
112
+ this.spawnRipple(host, rect.width / 2, rect.height / 2, Math.hypot(rect.width, rect.height));
113
+ }
114
+ /** Diameter that reaches the farthest corner from (x, y), so the ripple always covers the button. */
115
+ farthestCornerDiameter(rect, x, y) {
116
+ const corners = [
117
+ [0, 0],
118
+ [rect.width, 0],
119
+ [0, rect.height],
120
+ [rect.width, rect.height],
121
+ ];
122
+ const maxRadius = Math.max(...corners.map(([cx, cy]) => Math.hypot(cx - x, cy - y)));
123
+ return maxRadius * 2;
124
+ }
125
+ /**
126
+ * Spawns an expanding ripple circle. Created imperatively with inline styles and
127
+ * animated via the Web Animations API — Angular's emulated encapsulation does not
128
+ * reach dynamically created nodes, so it cannot rely on the component stylesheet.
129
+ */
130
+ spawnRipple(host, x, y, diameter) {
131
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
132
+ return;
133
+ }
134
+ const ripple = document.createElement('span');
135
+ Object.assign(ripple.style, {
136
+ position: 'absolute',
137
+ borderRadius: '50%',
138
+ backgroundColor: 'currentColor',
139
+ pointerEvents: 'none',
140
+ width: `${diameter}px`,
141
+ height: `${diameter}px`,
142
+ left: `${x - diameter / 2}px`,
143
+ top: `${y - diameter / 2}px`,
144
+ });
145
+ host.appendChild(ripple);
146
+ const animation = ripple.animate([
147
+ { transform: 'scale(0)', opacity: '0.16' },
148
+ { transform: 'scale(1)', opacity: '0' },
149
+ ], { duration: 450, easing: 'ease-out' });
150
+ animation.onfinish = () => ripple.remove();
56
151
  }
57
152
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
58
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ButtonComponent, isStandalone: true, selector: "compsych-button", inputs: { label: "label", variant: "variant", size: "size", icon: "icon", iconPosition: "iconPosition", type: "type", ariaLabel: "ariaLabel", iconOnly: ["iconOnly", "iconOnly", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], loading: ["loading", "loading", booleanAttribute] }, ngImport: i0, template: `
153
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ButtonComponent, isStandalone: true, selector: "compsych-button", inputs: { label: "label", variant: "variant", size: "size", leadingIcon: "leadingIcon", trailingIcon: "trailingIcon", type: "type", ariaLabel: "ariaLabel", iconOnly: ["iconOnly", "iconOnly", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], loading: ["loading", "loading", booleanAttribute], fullWidth: ["fullWidth", "fullWidth", booleanAttribute] }, host: { properties: { "class.full-width": "this.hostFullWidth" } }, usesOnChanges: true, ngImport: i0, template: `
59
154
  <button
60
155
  [class]="buttonClasses"
61
156
  [type]="type"
62
157
  [disabled]="disabled || loading"
63
158
  [attr.aria-busy]="loading ? 'true' : null"
64
159
  [attr.aria-label]="computedAriaLabel"
160
+ (pointerdown)="onPointerDown($event)"
161
+ (keydown)="onKeyDown($event)"
65
162
  >
66
- @if (icon && (iconOnly || iconPosition === 'leading')) {
67
- <span class="icon material-symbols-rounded" aria-hidden="true">{{ icon }}</span>
163
+ @if (loading) {
164
+ <span class="icon spinner" aria-hidden="true"></span>
165
+ } @else if (leadingIcon) {
166
+ <span class="icon" aria-hidden="true" [innerHTML]="leadingIconSvg"></span>
68
167
  }
69
168
  @if (!iconOnly) {
70
- <span class="label">{{ label }}</span>
71
- }
72
- @if (icon && !iconOnly && iconPosition === 'trailing') {
73
- <span class="icon material-symbols-rounded" aria-hidden="true">{{ icon }}</span>
169
+ <span class="label">{{ label }}<ng-content /></span>
74
170
  }
75
- @if (loading) {
76
- <span class="spinner-overlay" aria-hidden="true">
77
- <span class="icon spinner material-symbols-rounded">progress_activity</span>
78
- </span>
171
+ @if (trailingIcon && !iconOnly) {
172
+ <span class="icon" aria-hidden="true" [innerHTML]="trailingIconSvg"></span>
79
173
  }
80
174
  </button>
81
- `, isInline: true, styles: [":host{display:inline-flex}.cmp-btn{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;margin:0;border:none;border-radius:var(--border-radius-sys-radius-full);background:none;color:inherit;font:inherit;cursor:pointer;white-space:nowrap;position:relative;overflow:hidden;appearance:none;-webkit-appearance:none}.cmp-btn:before{content:\"\";position:absolute;inset:0;border-radius:inherit;background-color:currentColor;opacity:0;transition:opacity .15s ease;pointer-events:none}.cmp-btn:hover:before{opacity:.08}.cmp-btn:active:before{opacity:.1}.cmp-btn:focus-visible{outline:var(--border-width-sys-stroke-thick) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thick)}.cmp-btn:focus-visible:before{opacity:.12}.cmp-btn:disabled{cursor:not-allowed;pointer-events:none}.cmp-btn:disabled:not(.loading){opacity:.38}.icon{font-size:inherit;line-height:1;-webkit-user-select:none;user-select:none}.cmp-btn.icon-only{padding:0;aspect-ratio:1}.cmp-btn.icon-only.small{width:2rem}.cmp-btn.icon-only.medium{width:2.5rem}.cmp-btn.icon-only.large{width:3rem}.cmp-btn.icon-only.xlarge{width:3.5rem}.cmp-btn.loading>:not(.spinner-overlay){visibility:hidden}.spinner-overlay{position:absolute;inset:0;display:inline-flex;align-items:center;justify-content:center}.spinner{animation:cmp-btn-spin 1s linear infinite}@keyframes cmp-btn-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation-duration:2.4s}}.cmp-btn.small{gap:var(--spacing-padding-sys-padding-4);height:2rem;padding:var(--spacing-padding-sys-padding-4) var(--spacing-padding-sys-padding-12);font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);font-weight:var(--label-small-sys-font-weight-emphasized);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking)}.cmp-btn.medium{gap:var(--spacing-padding-sys-padding-8);height:2.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16);font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking)}.cmp-btn.large{gap:var(--spacing-padding-sys-padding-8);height:3rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.xlarge{gap:var(--spacing-padding-sys-padding-12);height:3.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.filled{background-color:var(--accent-primary-sys-primary);color:var(--accent-primary-sys-on-primary)}.cmp-btn.tonal{background-color:var(--add-on-primary-fixed-sys-primary-fixed-dim);color:var(--add-on-primary-fixed-sys-on-primary-fixed)}.cmp-btn.outlined{background-color:transparent;color:var(--surface-surface-sys-on-surface);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline)}.cmp-btn.elevated{background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);box-shadow:0 .125rem .25rem #00000014,0 .25rem 1rem var(--transparent-neutral-sys-black-10)}.cmp-btn.text{background-color:transparent;color:var(--surface-surface-sys-on-surface);padding-inline:0}.cmp-btn.danger{background-color:var(--error-sys-error);color:var(--error-sys-on-error)}.cmp-btn.danger-outlined{background-color:transparent;color:var(--error-sys-error);border:var(--border-width-sys-stroke-thin) solid var(--error-sys-error-container)}@media(forced-colors:active){.cmp-btn{border:1px solid ButtonText;forced-color-adjust:none}.cmp-btn.filled,.cmp-btn.tonal,.cmp-btn.danger{background:ButtonText;color:ButtonFace}.cmp-btn:focus-visible{outline:2px solid Highlight;outline-offset:2px}.cmp-btn:disabled:not(.loading){color:GrayText;border-color:GrayText}}\n"] });
175
+ `, isInline: true, styles: [":host{display:inline-flex}:host(.full-width){display:flex;width:100%}.cmp-btn{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;margin:0;border:none;border-radius:var(--border-radius-sys-radius-full);background:none;color:inherit;font:inherit;cursor:pointer;white-space:nowrap;position:relative;overflow:hidden;appearance:none;-webkit-appearance:none;transition:box-shadow .15s ease}.cmp-btn.full-width{width:100%}.cmp-btn:before{content:\"\";position:absolute;inset:0;border-radius:inherit;background-color:currentColor;opacity:0;transition:opacity .15s ease;pointer-events:none}.cmp-btn:hover:before{opacity:.08}.cmp-btn:active:before{opacity:.1}.cmp-btn.filled:hover:not(:disabled),.cmp-btn.danger:hover:not(:disabled){box-shadow:0 .25rem 1rem var(--transparent-neutral-sys-black-10),0 .125rem .25rem #00000014}.cmp-btn:focus-visible{outline:var(--border-width-sys-stroke-thick) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thick)}.cmp-btn:focus-visible:before{opacity:.12}.cmp-btn:disabled{pointer-events:none}.cmp-btn:disabled:not(.loading){cursor:not-allowed;background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface);border-color:transparent;box-shadow:none;opacity:.48}.cmp-btn.outlined:disabled:not(.loading),.cmp-btn.text:disabled:not(.loading),.cmp-btn.danger-outlined:disabled:not(.loading){background-color:transparent}.cmp-btn.outlined:disabled:not(.loading),.cmp-btn.danger-outlined:disabled:not(.loading){border-color:var(--outline-sys-outline-variant)}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:0;-webkit-user-select:none;user-select:none;width:1rem;height:1rem}.cmp-btn.large .icon{width:1.25rem;height:1.25rem}.cmp-btn.xlarge .icon{width:1.5rem;height:1.5rem}.icon svg{width:100%;height:100%}.cmp-btn.icon-only{padding:0;aspect-ratio:1}.cmp-btn.icon-only.small{width:2rem}.cmp-btn.icon-only.medium{width:2.5rem}.cmp-btn.icon-only.large{width:3rem}.cmp-btn.icon-only.xlarge{width:3.5rem}.spinner{border:2px solid currentColor;border-right-color:transparent;border-radius:50%;box-sizing:border-box;animation:cmp-btn-spin 1s linear infinite}@keyframes cmp-btn-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation-duration:2.4s}}.cmp-btn.small{gap:var(--spacing-padding-sys-padding-4);height:2rem;padding:var(--spacing-padding-sys-padding-4) var(--spacing-padding-sys-padding-12);font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);font-weight:var(--label-small-sys-font-weight-emphasized);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking)}.cmp-btn.medium{gap:var(--spacing-padding-sys-padding-8);height:2.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16);font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking)}.cmp-btn.large{gap:var(--spacing-padding-sys-padding-8);height:3rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.xlarge{gap:var(--spacing-padding-sys-padding-12);height:3.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.filled{background-color:var(--accent-primary-sys-primary);color:var(--accent-primary-sys-on-primary)}.cmp-btn.tonal{background-color:var(--add-on-primary-fixed-sys-primary-fixed-dim);color:var(--add-on-primary-fixed-sys-on-primary-fixed)}.cmp-btn.outlined{background-color:transparent;color:var(--surface-surface-sys-on-surface);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline)}.cmp-btn.elevated{background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);box-shadow:0 .125rem .25rem #00000014,0 .25rem 1rem var(--transparent-neutral-sys-black-10)}.cmp-btn.text{background-color:transparent;color:var(--surface-surface-sys-on-surface);padding-inline:0}.cmp-btn.danger{background-color:var(--error-sys-error);color:var(--error-sys-on-error)}.cmp-btn.danger-outlined{background-color:transparent;color:var(--error-sys-error);border:var(--border-width-sys-stroke-thin) solid var(--error-sys-error-container)}@media(forced-colors:active){.cmp-btn{border:1px solid ButtonText;forced-color-adjust:none}.cmp-btn.filled,.cmp-btn.tonal,.cmp-btn.danger{background:ButtonText;color:ButtonFace}.cmp-btn:focus-visible{outline:2px solid Highlight;outline-offset:2px}.cmp-btn:disabled:not(.loading){color:GrayText;border-color:GrayText;background:ButtonFace}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
82
176
  }
83
177
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ButtonComponent, decorators: [{
84
178
  type: Component,
85
- args: [{ selector: 'compsych-button', standalone: true, template: `
179
+ args: [{ selector: 'compsych-button', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
86
180
  <button
87
181
  [class]="buttonClasses"
88
182
  [type]="type"
89
183
  [disabled]="disabled || loading"
90
184
  [attr.aria-busy]="loading ? 'true' : null"
91
185
  [attr.aria-label]="computedAriaLabel"
186
+ (pointerdown)="onPointerDown($event)"
187
+ (keydown)="onKeyDown($event)"
92
188
  >
93
- @if (icon && (iconOnly || iconPosition === 'leading')) {
94
- <span class="icon material-symbols-rounded" aria-hidden="true">{{ icon }}</span>
189
+ @if (loading) {
190
+ <span class="icon spinner" aria-hidden="true"></span>
191
+ } @else if (leadingIcon) {
192
+ <span class="icon" aria-hidden="true" [innerHTML]="leadingIconSvg"></span>
95
193
  }
96
194
  @if (!iconOnly) {
97
- <span class="label">{{ label }}</span>
98
- }
99
- @if (icon && !iconOnly && iconPosition === 'trailing') {
100
- <span class="icon material-symbols-rounded" aria-hidden="true">{{ icon }}</span>
195
+ <span class="label">{{ label }}<ng-content /></span>
101
196
  }
102
- @if (loading) {
103
- <span class="spinner-overlay" aria-hidden="true">
104
- <span class="icon spinner material-symbols-rounded">progress_activity</span>
105
- </span>
197
+ @if (trailingIcon && !iconOnly) {
198
+ <span class="icon" aria-hidden="true" [innerHTML]="trailingIconSvg"></span>
106
199
  }
107
200
  </button>
108
- `, styles: [":host{display:inline-flex}.cmp-btn{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;margin:0;border:none;border-radius:var(--border-radius-sys-radius-full);background:none;color:inherit;font:inherit;cursor:pointer;white-space:nowrap;position:relative;overflow:hidden;appearance:none;-webkit-appearance:none}.cmp-btn:before{content:\"\";position:absolute;inset:0;border-radius:inherit;background-color:currentColor;opacity:0;transition:opacity .15s ease;pointer-events:none}.cmp-btn:hover:before{opacity:.08}.cmp-btn:active:before{opacity:.1}.cmp-btn:focus-visible{outline:var(--border-width-sys-stroke-thick) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thick)}.cmp-btn:focus-visible:before{opacity:.12}.cmp-btn:disabled{cursor:not-allowed;pointer-events:none}.cmp-btn:disabled:not(.loading){opacity:.38}.icon{font-size:inherit;line-height:1;-webkit-user-select:none;user-select:none}.cmp-btn.icon-only{padding:0;aspect-ratio:1}.cmp-btn.icon-only.small{width:2rem}.cmp-btn.icon-only.medium{width:2.5rem}.cmp-btn.icon-only.large{width:3rem}.cmp-btn.icon-only.xlarge{width:3.5rem}.cmp-btn.loading>:not(.spinner-overlay){visibility:hidden}.spinner-overlay{position:absolute;inset:0;display:inline-flex;align-items:center;justify-content:center}.spinner{animation:cmp-btn-spin 1s linear infinite}@keyframes cmp-btn-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation-duration:2.4s}}.cmp-btn.small{gap:var(--spacing-padding-sys-padding-4);height:2rem;padding:var(--spacing-padding-sys-padding-4) var(--spacing-padding-sys-padding-12);font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);font-weight:var(--label-small-sys-font-weight-emphasized);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking)}.cmp-btn.medium{gap:var(--spacing-padding-sys-padding-8);height:2.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16);font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking)}.cmp-btn.large{gap:var(--spacing-padding-sys-padding-8);height:3rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.xlarge{gap:var(--spacing-padding-sys-padding-12);height:3.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.filled{background-color:var(--accent-primary-sys-primary);color:var(--accent-primary-sys-on-primary)}.cmp-btn.tonal{background-color:var(--add-on-primary-fixed-sys-primary-fixed-dim);color:var(--add-on-primary-fixed-sys-on-primary-fixed)}.cmp-btn.outlined{background-color:transparent;color:var(--surface-surface-sys-on-surface);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline)}.cmp-btn.elevated{background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);box-shadow:0 .125rem .25rem #00000014,0 .25rem 1rem var(--transparent-neutral-sys-black-10)}.cmp-btn.text{background-color:transparent;color:var(--surface-surface-sys-on-surface);padding-inline:0}.cmp-btn.danger{background-color:var(--error-sys-error);color:var(--error-sys-on-error)}.cmp-btn.danger-outlined{background-color:transparent;color:var(--error-sys-error);border:var(--border-width-sys-stroke-thin) solid var(--error-sys-error-container)}@media(forced-colors:active){.cmp-btn{border:1px solid ButtonText;forced-color-adjust:none}.cmp-btn.filled,.cmp-btn.tonal,.cmp-btn.danger{background:ButtonText;color:ButtonFace}.cmp-btn:focus-visible{outline:2px solid Highlight;outline-offset:2px}.cmp-btn:disabled:not(.loading){color:GrayText;border-color:GrayText}}\n"] }]
201
+ `, styles: [":host{display:inline-flex}:host(.full-width){display:flex;width:100%}.cmp-btn{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;margin:0;border:none;border-radius:var(--border-radius-sys-radius-full);background:none;color:inherit;font:inherit;cursor:pointer;white-space:nowrap;position:relative;overflow:hidden;appearance:none;-webkit-appearance:none;transition:box-shadow .15s ease}.cmp-btn.full-width{width:100%}.cmp-btn:before{content:\"\";position:absolute;inset:0;border-radius:inherit;background-color:currentColor;opacity:0;transition:opacity .15s ease;pointer-events:none}.cmp-btn:hover:before{opacity:.08}.cmp-btn:active:before{opacity:.1}.cmp-btn.filled:hover:not(:disabled),.cmp-btn.danger:hover:not(:disabled){box-shadow:0 .25rem 1rem var(--transparent-neutral-sys-black-10),0 .125rem .25rem #00000014}.cmp-btn:focus-visible{outline:var(--border-width-sys-stroke-thick) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thick)}.cmp-btn:focus-visible:before{opacity:.12}.cmp-btn:disabled{pointer-events:none}.cmp-btn:disabled:not(.loading){cursor:not-allowed;background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface);border-color:transparent;box-shadow:none;opacity:.48}.cmp-btn.outlined:disabled:not(.loading),.cmp-btn.text:disabled:not(.loading),.cmp-btn.danger-outlined:disabled:not(.loading){background-color:transparent}.cmp-btn.outlined:disabled:not(.loading),.cmp-btn.danger-outlined:disabled:not(.loading){border-color:var(--outline-sys-outline-variant)}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:0;-webkit-user-select:none;user-select:none;width:1rem;height:1rem}.cmp-btn.large .icon{width:1.25rem;height:1.25rem}.cmp-btn.xlarge .icon{width:1.5rem;height:1.5rem}.icon svg{width:100%;height:100%}.cmp-btn.icon-only{padding:0;aspect-ratio:1}.cmp-btn.icon-only.small{width:2rem}.cmp-btn.icon-only.medium{width:2.5rem}.cmp-btn.icon-only.large{width:3rem}.cmp-btn.icon-only.xlarge{width:3.5rem}.spinner{border:2px solid currentColor;border-right-color:transparent;border-radius:50%;box-sizing:border-box;animation:cmp-btn-spin 1s linear infinite}@keyframes cmp-btn-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation-duration:2.4s}}.cmp-btn.small{gap:var(--spacing-padding-sys-padding-4);height:2rem;padding:var(--spacing-padding-sys-padding-4) var(--spacing-padding-sys-padding-12);font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);font-weight:var(--label-small-sys-font-weight-emphasized);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking)}.cmp-btn.medium{gap:var(--spacing-padding-sys-padding-8);height:2.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-16);font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking)}.cmp-btn.large{gap:var(--spacing-padding-sys-padding-8);height:3rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.xlarge{gap:var(--spacing-padding-sys-padding-12);height:3.5rem;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.cmp-btn.filled{background-color:var(--accent-primary-sys-primary);color:var(--accent-primary-sys-on-primary)}.cmp-btn.tonal{background-color:var(--add-on-primary-fixed-sys-primary-fixed-dim);color:var(--add-on-primary-fixed-sys-on-primary-fixed)}.cmp-btn.outlined{background-color:transparent;color:var(--surface-surface-sys-on-surface);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline)}.cmp-btn.elevated{background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);box-shadow:0 .125rem .25rem #00000014,0 .25rem 1rem var(--transparent-neutral-sys-black-10)}.cmp-btn.text{background-color:transparent;color:var(--surface-surface-sys-on-surface);padding-inline:0}.cmp-btn.danger{background-color:var(--error-sys-error);color:var(--error-sys-on-error)}.cmp-btn.danger-outlined{background-color:transparent;color:var(--error-sys-error);border:var(--border-width-sys-stroke-thin) solid var(--error-sys-error-container)}@media(forced-colors:active){.cmp-btn{border:1px solid ButtonText;forced-color-adjust:none}.cmp-btn.filled,.cmp-btn.tonal,.cmp-btn.danger{background:ButtonText;color:ButtonFace}.cmp-btn:focus-visible{outline:2px solid Highlight;outline-offset:2px}.cmp-btn:disabled:not(.loading){color:GrayText;border-color:GrayText;background:ButtonFace}}\n"] }]
109
202
  }], propDecorators: { label: [{
110
203
  type: Input
111
204
  }], variant: [{
112
205
  type: Input
113
206
  }], size: [{
114
207
  type: Input
115
- }], icon: [{
208
+ }], leadingIcon: [{
116
209
  type: Input
117
- }], iconPosition: [{
210
+ }], trailingIcon: [{
118
211
  type: Input
119
212
  }], type: [{
120
213
  type: Input
@@ -129,5 +222,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
129
222
  }], loading: [{
130
223
  type: Input,
131
224
  args: [{ transform: booleanAttribute }]
225
+ }], fullWidth: [{
226
+ type: Input,
227
+ args: [{ transform: booleanAttribute }]
228
+ }], hostFullWidth: [{
229
+ type: HostBinding,
230
+ args: ['class.full-width']
132
231
  }] } });
133
232
  //# sourceMappingURL=button.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/button/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;;AAqBnE;;;GAGG;AA8BH,MAAM,OAAO,eAAe;IAC1B,yCAAyC;IAChC,KAAK,GAAG,EAAE,CAAC;IACpB,0DAA0D;IACjD,OAAO,GAAkB,QAAQ,CAAC;IAC3C,0FAA0F;IACjF,IAAI,GAAe,OAAO,CAAC;IACpC,gGAAgG;IACvF,IAAI,CAAU;IACvB,0EAA0E;IACjE,YAAY,GAAuB,SAAS,CAAC;IACtD,iGAAiG;IACxF,IAAI,GAAe,QAAQ,CAAC;IACrC;;;OAGG;IACM,SAAS,CAAU;IAE5B,yGAAyG;IACjE,QAAQ,GAAG,KAAK,CAAC;IACzD,mFAAmF;IAC3C,QAAQ,GAAG,KAAK,CAAC;IACzD;;;;;OAKG;IACqC,OAAO,GAAG,KAAK,CAAC;IAExD,qEAAqE;IACrE,IAAI,aAAa;QACf,OAAO;YACL,SAAS;YACT,IAAI,CAAC,OAAO;YACZ,IAAI,CAAC,IAAI;YACT,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE;YAChC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;SAC9B;aACE,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IAED,6FAA6F;IAC7F,IAAI,iBAAiB;QACnB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC,SAAS,CAAC;QACxB,CAAC;QACD,qFAAqF;QACrF,wFAAwF;QACxF,OAAO,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACnE,CAAC;wGApDU,eAAe;4FAAf,eAAe,oOAoBN,gBAAgB,sCAEhB,gBAAgB,mCAOhB,gBAAgB,6BAvD1B;;;;;;;;;;;;;;;;;;;;;;;GAuBT;;4FAGU,eAAe;kBA7B3B,SAAS;+BACE,iBAAiB,cACf,IAAI,YACN;;;;;;;;;;;;;;;;;;;;;;;GAuBT;;sBAKA,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAKL,KAAK;;sBAGL,KAAK;uBAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE;;sBAErC,KAAK;uBAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE;;sBAOrC,KAAK;uBAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE","sourcesContent":["import { Component, Input, booleanAttribute } from '@angular/core';\r\n\r\n/** Visual style of a `compsych-button`. Controls color, border, and elevation. */\r\nexport type ButtonVariant =\r\n | 'filled'\r\n | 'tonal'\r\n | 'outlined'\r\n | 'elevated'\r\n | 'text'\r\n | 'danger'\r\n | 'danger-outlined';\r\n\r\n/** Button dimensions — controls height, padding, and font size. */\r\nexport type ButtonSize = 'small' | 'medium' | 'large' | 'xlarge';\r\n\r\n/** Placement of `icon` relative to the label. */\r\nexport type ButtonIconPosition = 'leading' | 'trailing';\r\n\r\n/** Native button behaviour. `'submit'`/`'reset'` make the component a real form control. */\r\nexport type ButtonType = 'button' | 'submit' | 'reset';\r\n\r\n/**\r\n * A themeable button that renders a native `<button>` element, so it is fully\r\n * form-submittable and keyboard/screen-reader accessible out of the box.\r\n */\r\n@Component({\r\n selector: 'compsych-button',\r\n standalone: true,\r\n template: `\r\n <button\r\n [class]=\"buttonClasses\"\r\n [type]=\"type\"\r\n [disabled]=\"disabled || loading\"\r\n [attr.aria-busy]=\"loading ? 'true' : null\"\r\n [attr.aria-label]=\"computedAriaLabel\"\r\n >\r\n @if (icon && (iconOnly || iconPosition === 'leading')) {\r\n <span class=\"icon material-symbols-rounded\" aria-hidden=\"true\">{{ icon }}</span>\r\n }\r\n @if (!iconOnly) {\r\n <span class=\"label\">{{ label }}</span>\r\n }\r\n @if (icon && !iconOnly && iconPosition === 'trailing') {\r\n <span class=\"icon material-symbols-rounded\" aria-hidden=\"true\">{{ icon }}</span>\r\n }\r\n @if (loading) {\r\n <span class=\"spinner-overlay\" aria-hidden=\"true\">\r\n <span class=\"icon spinner material-symbols-rounded\">progress_activity</span>\r\n </span>\r\n }\r\n </button>\r\n `,\r\n styleUrl: './button.css',\r\n})\r\nexport class ButtonComponent {\r\n /** Text label rendered on the button. */\r\n @Input() label = '';\r\n /** Visual style of the button. Defaults to `'filled'`. */\r\n @Input() variant: ButtonVariant = 'filled';\r\n /** Button dimensions — controls height, padding, and font size. Defaults to `'small'`. */\r\n @Input() size: ButtonSize = 'small';\r\n /** Material Symbols icon name (e.g. `'search'`, `'arrow_forward'`). Omit to show label only. */\r\n @Input() icon?: string;\r\n /** Placement of `icon` relative to the label. Defaults to `'leading'`. */\r\n @Input() iconPosition: ButtonIconPosition = 'leading';\r\n /** Native button type. Use `'submit'` inside a `<form>` to submit it. Defaults to `'button'`. */\r\n @Input() type: ButtonType = 'button';\r\n /**\r\n * Accessible name. Required for icon-only buttons since the icon is decorative\r\n * (`aria-hidden`). When `iconOnly` is set and this is omitted, `label` is used.\r\n */\r\n @Input() ariaLabel?: string;\r\n\r\n /** Hides the label and renders a square icon-only button. Requires `icon`. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) iconOnly = false;\r\n /** Disables the button — non-interactive and dimmed. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) disabled = false;\r\n /**\r\n * Loading state. Replaces the content with a centered spinner while preserving\r\n * the button's width, blocks re-clicks, and exposes `aria-busy=\"true\"`. The\r\n * variant's colours are retained (unlike `disabled`, which dims the button).\r\n * Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) loading = false;\r\n\r\n /** Space-separated class list applied to the inner native button. */\r\n get buttonClasses(): string {\r\n return [\r\n 'cmp-btn',\r\n this.variant,\r\n this.size,\r\n this.iconOnly ? 'icon-only' : '',\r\n this.loading ? 'loading' : '',\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n /** Resolves the accessible name, guaranteeing icon-only and loading buttons are labelled. */\r\n get computedAriaLabel(): string | null {\r\n if (this.ariaLabel) {\r\n return this.ariaLabel;\r\n }\r\n // Icon-only buttons have no visible text; while loading the label is hidden from the\r\n // a11y tree. In both cases fall back to `label` so the button keeps an accessible name.\r\n return this.iconOnly || this.loading ? this.label || null : null;\r\n }\r\n}\r\n"]}
1
+ {"version":3,"file":"button.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/button/button.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,SAAS,EACT,WAAW,EACX,KAAK,EAEL,gBAAgB,EAChB,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAY,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,8DAA8D,CAAC;;AAkB5F,4EAA4E;AAC5E,MAAM,wBAAwB,GAA+B;IAC3D,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;CACX,CAAC;AAEF;;;;;GAKG;AA8BH,MAAM,OAAO,eAAe;IACT,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IAElD,yCAAyC;IAChC,KAAK,GAAG,EAAE,CAAC;IACpB,0DAA0D;IACjD,OAAO,GAAkB,QAAQ,CAAC;IAC3C,qGAAqG;IAC5F,IAAI,GAAe,OAAO,CAAC;IACpC,oGAAoG;IAC3F,WAAW,CAAU;IAC9B,2GAA2G;IAClG,YAAY,CAAU;IAC/B,iGAAiG;IACxF,IAAI,GAAe,QAAQ,CAAC;IACrC;;;OAGG;IACM,SAAS,CAAU;IAE5B,4GAA4G;IACpE,QAAQ,GAAG,KAAK,CAAC;IACzD,uGAAuG;IAC/D,QAAQ,GAAG,KAAK,CAAC;IACzD;;;;OAIG;IACqC,OAAO,GAAG,KAAK,CAAC;IACxD,0FAA0F;IAClD,SAAS,GAAG,KAAK,CAAC;IAE1D,2DAA2D;IAC3D,IAAqC,aAAa;QAChD,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,mGAAmG;IACzF,cAAc,GAAoB,IAAI,CAAC;IACvC,eAAe,GAAoB,IAAI,CAAC;IAElD,WAAW;QACT,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACpF,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzF,CAAC;IAED,qEAAqE;IACrE,IAAI,aAAa;QACf,OAAO;YACL,SAAS;YACT,IAAI,CAAC,OAAO;YACZ,IAAI,CAAC,IAAI;YACT,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE;YAChC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;YAC7B,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;SACnC;aACE,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IAED,0GAA0G;IAC1G,IAAI,iBAAiB;QACnB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC,SAAS,CAAC;QACxB,CAAC;QACD,kFAAkF;QAClF,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,IAAY;QAC/B,MAAM,IAAI,GAAG,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,eAAe,IAAI,aAAa,IAAI,IAAI,CAAC,CAAC;QAClG,OAAO,IAAI,CAAC,SAAS,CAAC,uBAAuB,CAAC,GAAG,CAAC,CAAC;IACrD,CAAC;IAED,+EAA+E;IAC/E,aAAa,CAAC,KAAmB;QAC/B,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxD,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,aAA4B,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC;QACpC,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC;QACnC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,sBAAsB,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,qFAAqF;IACrF,SAAS,CAAC,KAAoB;QAC5B,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAClD,OAAO;QACT,CAAC;QACD,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;YAC/C,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,aAA4B,CAAC;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC1C,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/F,CAAC;IAED,qGAAqG;IAC7F,sBAAsB,CAAC,IAAa,EAAE,CAAS,EAAE,CAAS;QAChE,MAAM,OAAO,GAA4B;YACvC,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YACf,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC;YAChB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC;SAC1B,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACrF,OAAO,SAAS,GAAG,CAAC,CAAC;IACvB,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,IAAiB,EAAE,CAAS,EAAE,CAAS,EAAE,QAAgB;QAC3E,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC,kCAAkC,CAAC,CAAC,OAAO,EAAE,CAAC;YACpE,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE;YAC1B,QAAQ,EAAE,UAAU;YACpB,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,cAAc;YAC/B,aAAa,EAAE,MAAM;YACrB,KAAK,EAAE,GAAG,QAAQ,IAAI;YACtB,MAAM,EAAE,GAAG,QAAQ,IAAI;YACvB,IAAI,EAAE,GAAG,CAAC,GAAG,QAAQ,GAAG,CAAC,IAAI;YAC7B,GAAG,EAAE,GAAG,CAAC,GAAG,QAAQ,GAAG,CAAC,IAAI;SACG,CAAC,CAAC;QACnC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACzB,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAC9B;YACE,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE;YAC1C,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE;SACxC,EACD,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,CACtC,CAAC;QACF,SAAS,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;IAC7C,CAAC;wGApJU,eAAe;4FAAf,eAAe,kPAsBN,gBAAgB,sCAEhB,gBAAgB,mCAMhB,gBAAgB,yCAEhB,gBAAgB,sHAzD1B;;;;;;;;;;;;;;;;;;;;;;GAsBT;;4FAGU,eAAe;kBA7B3B,SAAS;+BACE,iBAAiB,cACf,IAAI,mBACC,uBAAuB,CAAC,MAAM,YACrC;;;;;;;;;;;;;;;;;;;;;;GAsBT;;sBAOA,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAEL,KAAK;;sBAKL,KAAK;;sBAGL,KAAK;uBAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE;;sBAErC,KAAK;uBAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE;;sBAMrC,KAAK;uBAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE;;sBAErC,KAAK;uBAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE;;sBAGrC,WAAW;uBAAC,kBAAkB","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n HostBinding,\r\n Input,\r\n OnChanges,\r\n booleanAttribute,\r\n inject,\r\n} from '@angular/core';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\n\r\n/** Visual style of a `compsych-button`. Controls color, border, and elevation. */\r\nexport type ButtonVariant =\r\n | 'filled'\r\n | 'tonal'\r\n | 'outlined'\r\n | 'elevated'\r\n | 'text'\r\n | 'danger'\r\n | 'danger-outlined';\r\n\r\n/** Button dimensions — controls height, padding, font size, and icon size. */\r\nexport type ButtonSize = 'small' | 'medium' | 'large' | 'xlarge';\r\n\r\n/** Native button behaviour. `'submit'`/`'reset'` make the component a real form control. */\r\nexport type ButtonType = 'button' | 'submit' | 'reset';\r\n\r\n/** Icon pixel size per button size (per design: sm/md 16, lg 20, xl 24). */\r\nconst ICON_SIZE_BY_BUTTON_SIZE: Record<ButtonSize, number> = {\r\n small: 16,\r\n medium: 16,\r\n large: 20,\r\n xlarge: 24,\r\n};\r\n\r\n/**\r\n * A themeable button that renders a native `<button>` element, so it is fully\r\n * form-submittable and keyboard/screen-reader accessible out of the box. Icons\r\n * are Lucide glyphs (matching the rest of the library); a button may carry a\r\n * leading icon, a trailing icon, or both.\r\n */\r\n@Component({\r\n selector: 'compsych-button',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n template: `\r\n <button\r\n [class]=\"buttonClasses\"\r\n [type]=\"type\"\r\n [disabled]=\"disabled || loading\"\r\n [attr.aria-busy]=\"loading ? 'true' : null\"\r\n [attr.aria-label]=\"computedAriaLabel\"\r\n (pointerdown)=\"onPointerDown($event)\"\r\n (keydown)=\"onKeyDown($event)\"\r\n >\r\n @if (loading) {\r\n <span class=\"icon spinner\" aria-hidden=\"true\"></span>\r\n } @else if (leadingIcon) {\r\n <span class=\"icon\" aria-hidden=\"true\" [innerHTML]=\"leadingIconSvg\"></span>\r\n }\r\n @if (!iconOnly) {\r\n <span class=\"label\">{{ label }}<ng-content /></span>\r\n }\r\n @if (trailingIcon && !iconOnly) {\r\n <span class=\"icon\" aria-hidden=\"true\" [innerHTML]=\"trailingIconSvg\"></span>\r\n }\r\n </button>\r\n `,\r\n styleUrl: './button.css',\r\n})\r\nexport class ButtonComponent implements OnChanges {\r\n private readonly sanitizer = inject(DomSanitizer);\r\n\r\n /** Text label rendered on the button. */\r\n @Input() label = '';\r\n /** Visual style of the button. Defaults to `'filled'`. */\r\n @Input() variant: ButtonVariant = 'filled';\r\n /** Button dimensions — controls height, padding, font size, and icon size. Defaults to `'small'`. */\r\n @Input() size: ButtonSize = 'small';\r\n /** Lucide icon name shown before the label (e.g. `'house'`). Swapped for a spinner when loading. */\r\n @Input() leadingIcon?: string;\r\n /** Lucide icon name shown after the label (e.g. `'chevron-right'`). Can be combined with `leadingIcon`. */\r\n @Input() trailingIcon?: string;\r\n /** Native button type. Use `'submit'` inside a `<form>` to submit it. Defaults to `'button'`. */\r\n @Input() type: ButtonType = 'button';\r\n /**\r\n * Accessible name. Required for icon-only buttons since the icon is decorative.\r\n * When `iconOnly` or `loading` is set and this is omitted, `label` is used.\r\n */\r\n @Input() ariaLabel?: string;\r\n\r\n /** Hides the label and renders a square icon-only button. Uses `leadingIcon`. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) iconOnly = false;\r\n /** Disables the button — non-interactive and rendered in a neutral grey. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) disabled = false;\r\n /**\r\n * Loading state. Swaps the leading icon for a spinner and suppresses activation\r\n * (native `disabled` blocks click/keyboard), while keeping the label and exposing\r\n * `aria-busy=\"true\"`. Retains the variant's colours. Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) loading = false;\r\n /** Stretches the button to the full width of its container. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) fullWidth = false;\r\n\r\n /** Mirrors `fullWidth` onto the host so it can stretch. */\r\n @HostBinding('class.full-width') get hostFullWidth(): boolean {\r\n return this.fullWidth;\r\n }\r\n\r\n /** Pre-sanitized icon markup, recomputed on input change (avoids re-sanitizing every CD cycle). */\r\n protected leadingIconSvg: SafeHtml | null = null;\r\n protected trailingIconSvg: SafeHtml | null = null;\r\n\r\n ngOnChanges(): void {\r\n this.leadingIconSvg = this.leadingIcon ? this.buildIconSvg(this.leadingIcon) : null;\r\n this.trailingIconSvg = this.trailingIcon ? this.buildIconSvg(this.trailingIcon) : null;\r\n }\r\n\r\n /** Space-separated class list applied to the inner native button. */\r\n get buttonClasses(): string {\r\n return [\r\n 'cmp-btn',\r\n this.variant,\r\n this.size,\r\n this.iconOnly ? 'icon-only' : '',\r\n this.loading ? 'loading' : '',\r\n this.fullWidth ? 'full-width' : '',\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n /** Resolves the accessible name. Icon-only buttons have no visible text, so they fall back to `label`. */\r\n get computedAriaLabel(): string | null {\r\n if (this.ariaLabel) {\r\n return this.ariaLabel;\r\n }\r\n // Loading keeps the visible label, so only icon-only needs an explicit name here.\r\n return this.iconOnly ? this.label || null : null;\r\n }\r\n\r\n /**\r\n * Looks up a Lucide icon by name and injects an explicit width/height so the SVG\r\n * renders at the correct size when injected via `[innerHTML]` (Angular's CSS\r\n * encapsulation does not reach dynamically injected content).\r\n */\r\n private buildIconSvg(name: string): SafeHtml {\r\n const size = ICON_SIZE_BY_BUTTON_SIZE[this.size];\r\n const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n\r\n /** M3 pressed ripple: pointer-down spawns a ripple at the pointer position. */\r\n onPointerDown(event: PointerEvent): void {\r\n if (this.disabled || this.loading || event.button !== 0) {\r\n return;\r\n }\r\n const host = event.currentTarget as HTMLElement;\r\n const rect = host.getBoundingClientRect();\r\n const x = event.clientX - rect.left;\r\n const y = event.clientY - rect.top;\r\n this.spawnRipple(host, x, y, this.farthestCornerDiameter(rect, x, y));\r\n }\r\n\r\n /** M3 pressed ripple: keyboard activation (Enter/Space) spawns a centered ripple. */\r\n onKeyDown(event: KeyboardEvent): void {\r\n if (this.disabled || this.loading || event.repeat) {\r\n return;\r\n }\r\n if (event.key !== 'Enter' && event.key !== ' ') {\r\n return;\r\n }\r\n const host = event.currentTarget as HTMLElement;\r\n const rect = host.getBoundingClientRect();\r\n this.spawnRipple(host, rect.width / 2, rect.height / 2, Math.hypot(rect.width, rect.height));\r\n }\r\n\r\n /** Diameter that reaches the farthest corner from (x, y), so the ripple always covers the button. */\r\n private farthestCornerDiameter(rect: DOMRect, x: number, y: number): number {\r\n const corners: Array<[number, number]> = [\r\n [0, 0],\r\n [rect.width, 0],\r\n [0, rect.height],\r\n [rect.width, rect.height],\r\n ];\r\n const maxRadius = Math.max(...corners.map(([cx, cy]) => Math.hypot(cx - x, cy - y)));\r\n return maxRadius * 2;\r\n }\r\n\r\n /**\r\n * Spawns an expanding ripple circle. Created imperatively with inline styles and\r\n * animated via the Web Animations API — Angular's emulated encapsulation does not\r\n * reach dynamically created nodes, so it cannot rely on the component stylesheet.\r\n */\r\n private spawnRipple(host: HTMLElement, x: number, y: number, diameter: number): void {\r\n if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {\r\n return;\r\n }\r\n const ripple = document.createElement('span');\r\n Object.assign(ripple.style, {\r\n position: 'absolute',\r\n borderRadius: '50%',\r\n backgroundColor: 'currentColor',\r\n pointerEvents: 'none',\r\n width: `${diameter}px`,\r\n height: `${diameter}px`,\r\n left: `${x - diameter / 2}px`,\r\n top: `${y - diameter / 2}px`,\r\n } as Partial<CSSStyleDeclaration>);\r\n host.appendChild(ripple);\r\n const animation = ripple.animate(\r\n [\r\n { transform: 'scale(0)', opacity: '0.16' },\r\n { transform: 'scale(1)', opacity: '0' },\r\n ],\r\n { duration: 450, easing: 'ease-out' },\r\n );\r\n animation.onfinish = () => ripple.remove();\r\n }\r\n}\r\n"]}
@@ -0,0 +1,87 @@
1
+ import { ChangeDetectionStrategy, Component, HostListener, Input, afterNextRender, signal, } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Footer — bottom bar from the ComPsych Design System.
5
+ *
6
+ * Displays a copyright notice and an optional list of navigation links.
7
+ * On initialisation the component measures the natural widths of the copyright
8
+ * and links to establish a breakpoint. On every window resize it compares the
9
+ * host width against that breakpoint and switches between a row layout
10
+ * (copyright left, links right) and a stacked, centered column layout.
11
+ */
12
+ export class FooterComponent {
13
+ el;
14
+ /**
15
+ * Navigation links rendered alongside the copyright notice.
16
+ * The layout switches automatically based on their combined natural width.
17
+ */
18
+ links = [];
19
+ /**
20
+ * Copyright notice generated from the current calendar year.
21
+ * Left-aligned in the row layout and centered when stacked.
22
+ */
23
+ copyright = `Copyright \u00a9 ${new Date().getFullYear()} ComPsych\u00ae. All rights reserved.`;
24
+ /**
25
+ * Reflects whether the row layout is active.
26
+ * Signal so OnPush picks up DOM-driven updates from async callbacks.
27
+ */
28
+ isRow = signal(false, ...(ngDevMode ? [{ debugName: "isRow" }] : /* istanbul ignore next */ []));
29
+ /** Combined natural width of copyright + links + gap + padding. */
30
+ breakpoint = 0;
31
+ constructor(el) {
32
+ this.el = el;
33
+ afterNextRender(() => {
34
+ this.measureBreakpoint();
35
+ this.checkLayout();
36
+ });
37
+ }
38
+ ngOnChanges() {
39
+ // Wait for Angular to update the DOM with the new links before re-measuring.
40
+ requestAnimationFrame(() => {
41
+ this.measureBreakpoint();
42
+ this.checkLayout();
43
+ });
44
+ }
45
+ onResize() {
46
+ this.checkLayout();
47
+ }
48
+ /**
49
+ * Measures the natural widths of the copyright and links elements once and
50
+ * stores their sum (plus padding and gap) as the layout breakpoint.
51
+ */
52
+ measureBreakpoint() {
53
+ const host = this.el.nativeElement;
54
+ const copyrightEl = host.querySelector('.compsych-footer__copyright');
55
+ const linksEl = host.querySelector('.compsych-footer__links');
56
+ if (!copyrightEl)
57
+ return;
58
+ const style = getComputedStyle(host);
59
+ const paddingInline = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
60
+ const gap = parseFloat(style.columnGap) || 0;
61
+ this.breakpoint =
62
+ copyrightEl.scrollWidth +
63
+ (linksEl ? linksEl.scrollWidth + gap : 0) +
64
+ paddingInline;
65
+ }
66
+ /** Switches to the row layout when the host is wide enough for both children. */
67
+ checkLayout() {
68
+ if (!this.breakpoint)
69
+ return;
70
+ this.isRow.set(this.el.nativeElement.offsetWidth >= this.breakpoint);
71
+ }
72
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FooterComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
73
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: FooterComponent, isStandalone: true, selector: "compsych-footer", inputs: { links: "links" }, host: { attributes: { "role": "contentinfo" }, listeners: { "window:resize": "onResize()" }, properties: { "class.compsych-footer--row": "isRow()" } }, usesOnChanges: true, ngImport: i0, template: "@if (links.length) {\n <nav class=\"compsych-footer__links\" aria-label=\"Footer navigation\">\n <ul role=\"list\">\n @for (link of links; track link.href; let first = $first) {\n <li>\n @if (!first) {\n <span class=\"compsych-footer__divider\" aria-hidden=\"true\"></span>\n }\n <a class=\"compsych-footer__link\" [href]=\"link.href\">{{ link.label }}</a>\n </li>\n }\n </ul>\n </nav>\n}\n\n<p class=\"compsych-footer__copyright\">{{ copyright }}</p>\n", styles: [":host{display:flex;flex-direction:column;align-items:center;gap:var(--spacing-padding-sys-padding-8);min-height:var(--sys-size-xl);padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);background:var(--surface-surface-container-sys-surface-container-lowest);border-top:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);box-sizing:border-box;font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking);color:var(--surface-surface-sys-on-surface-variant)}:host(.compsych-footer--row){flex-direction:row;justify-content:space-between;align-items:center}.compsych-footer__copyright{margin:0;text-align:center;font-weight:var(--label-small-sys-font-weight);overflow-wrap:anywhere}:host(.compsych-footer--row) .compsych-footer__copyright{text-align:left;order:-1}.compsych-footer__links{display:flex;align-items:center}.compsych-footer__links ul{list-style:none;margin:0;padding:0;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-16)}.compsych-footer__links li{display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-16)}.compsych-footer__link{color:var(--surface-surface-sys-on-surface-variant);font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);font-weight:var(--label-small-sys-font-weight-emphasized);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking);text-decoration:none;overflow-wrap:anywhere}.compsych-footer__link:hover{text-decoration:underline}.compsych-footer__link:focus-visible{outline:var(--border-width-sys-stroke-thick) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thick);border-radius:var(--border-radius-sys-radius-xs)}.compsych-footer__divider{display:block;width:0;height:var(--sys-size-xs);border-left:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-fixed);flex-shrink:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
74
+ }
75
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FooterComponent, decorators: [{
76
+ type: Component,
77
+ args: [{ selector: 'compsych-footer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [], host: {
78
+ role: 'contentinfo',
79
+ '[class.compsych-footer--row]': 'isRow()',
80
+ }, template: "@if (links.length) {\n <nav class=\"compsych-footer__links\" aria-label=\"Footer navigation\">\n <ul role=\"list\">\n @for (link of links; track link.href; let first = $first) {\n <li>\n @if (!first) {\n <span class=\"compsych-footer__divider\" aria-hidden=\"true\"></span>\n }\n <a class=\"compsych-footer__link\" [href]=\"link.href\">{{ link.label }}</a>\n </li>\n }\n </ul>\n </nav>\n}\n\n<p class=\"compsych-footer__copyright\">{{ copyright }}</p>\n", styles: [":host{display:flex;flex-direction:column;align-items:center;gap:var(--spacing-padding-sys-padding-8);min-height:var(--sys-size-xl);padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-24);background:var(--surface-surface-container-sys-surface-container-lowest);border-top:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);box-sizing:border-box;font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking);color:var(--surface-surface-sys-on-surface-variant)}:host(.compsych-footer--row){flex-direction:row;justify-content:space-between;align-items:center}.compsych-footer__copyright{margin:0;text-align:center;font-weight:var(--label-small-sys-font-weight);overflow-wrap:anywhere}:host(.compsych-footer--row) .compsych-footer__copyright{text-align:left;order:-1}.compsych-footer__links{display:flex;align-items:center}.compsych-footer__links ul{list-style:none;margin:0;padding:0;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-16)}.compsych-footer__links li{display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-16)}.compsych-footer__link{color:var(--surface-surface-sys-on-surface-variant);font-family:var(--label-small-sys-font-family);font-size:var(--label-small-sys-font-size);font-weight:var(--label-small-sys-font-weight-emphasized);line-height:var(--label-small-sys-line-height);letter-spacing:var(--label-small-sys-tracking);text-decoration:none;overflow-wrap:anywhere}.compsych-footer__link:hover{text-decoration:underline}.compsych-footer__link:focus-visible{outline:var(--border-width-sys-stroke-thick) solid var(--add-on-primary-fixed-sys-on-primary-fixed-variant);outline-offset:var(--border-width-sys-stroke-thick);border-radius:var(--border-radius-sys-radius-xs)}.compsych-footer__divider{display:block;width:0;height:var(--sys-size-xs);border-left:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-fixed);flex-shrink:0}\n"] }]
81
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { links: [{
82
+ type: Input
83
+ }], onResize: [{
84
+ type: HostListener,
85
+ args: ['window:resize']
86
+ }] } });
87
+ //# sourceMappingURL=footer.js.map