@compsych-ui-components/angular 7.0.0 → 7.0.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, 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"]}
@@ -150,55 +150,55 @@ export class ButtonComponent {
150
150
  animation.onfinish = () => ripple.remove();
151
151
  }
152
152
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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: `
154
- <button
155
- [class]="buttonClasses"
156
- [type]="type"
157
- [disabled]="disabled || loading"
158
- [attr.aria-busy]="loading ? 'true' : null"
159
- [attr.aria-label]="computedAriaLabel"
160
- (pointerdown)="onPointerDown($event)"
161
- (keydown)="onKeyDown($event)"
162
- >
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>
167
- }
168
- @if (!iconOnly) {
169
- <span class="label">{{ label }}<ng-content /></span>
170
- }
171
- @if (trailingIcon && !iconOnly) {
172
- <span class="icon" aria-hidden="true" [innerHTML]="trailingIconSvg"></span>
173
- }
174
- </button>
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 });
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: `
154
+ <button
155
+ [class]="buttonClasses"
156
+ [type]="type"
157
+ [disabled]="disabled || loading"
158
+ [attr.aria-busy]="loading ? 'true' : null"
159
+ [attr.aria-label]="computedAriaLabel"
160
+ (pointerdown)="onPointerDown($event)"
161
+ (keydown)="onKeyDown($event)"
162
+ >
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>
167
+ }
168
+ @if (!iconOnly) {
169
+ <span class="label">{{ label }}<ng-content /></span>
170
+ }
171
+ @if (trailingIcon && !iconOnly) {
172
+ <span class="icon" aria-hidden="true" [innerHTML]="trailingIconSvg"></span>
173
+ }
174
+ </button>
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:32px;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:40px;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:48px;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:56px;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 });
176
176
  }
177
177
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ButtonComponent, decorators: [{
178
178
  type: Component,
179
- args: [{ selector: 'compsych-button', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
180
- <button
181
- [class]="buttonClasses"
182
- [type]="type"
183
- [disabled]="disabled || loading"
184
- [attr.aria-busy]="loading ? 'true' : null"
185
- [attr.aria-label]="computedAriaLabel"
186
- (pointerdown)="onPointerDown($event)"
187
- (keydown)="onKeyDown($event)"
188
- >
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>
193
- }
194
- @if (!iconOnly) {
195
- <span class="label">{{ label }}<ng-content /></span>
196
- }
197
- @if (trailingIcon && !iconOnly) {
198
- <span class="icon" aria-hidden="true" [innerHTML]="trailingIconSvg"></span>
199
- }
200
- </button>
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"] }]
179
+ args: [{ selector: 'compsych-button', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
180
+ <button
181
+ [class]="buttonClasses"
182
+ [type]="type"
183
+ [disabled]="disabled || loading"
184
+ [attr.aria-busy]="loading ? 'true' : null"
185
+ [attr.aria-label]="computedAriaLabel"
186
+ (pointerdown)="onPointerDown($event)"
187
+ (keydown)="onKeyDown($event)"
188
+ >
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>
193
+ }
194
+ @if (!iconOnly) {
195
+ <span class="label">{{ label }}<ng-content /></span>
196
+ }
197
+ @if (trailingIcon && !iconOnly) {
198
+ <span class="icon" aria-hidden="true" [innerHTML]="trailingIconSvg"></span>
199
+ }
200
+ </button>
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:32px;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:40px;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:48px;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:56px;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"] }]
202
202
  }], propDecorators: { label: [{
203
203
  type: Input
204
204
  }], variant: [{
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/button/button.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n HostBinding,\n Input,\n OnChanges,\n booleanAttribute,\n inject,\n} from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\n\n/** Visual style of a `compsych-button`. Controls color, border, and elevation. */\nexport type ButtonVariant =\n | 'filled'\n | 'tonal'\n | 'outlined'\n | 'elevated'\n | 'text'\n | 'danger'\n | 'danger-outlined';\n\n/** Button dimensions — controls height, padding, font size, and icon size. */\nexport type ButtonSize = 'small' | 'medium' | 'large' | 'xlarge';\n\n/** Native button behaviour. `'submit'`/`'reset'` make the component a real form control. */\nexport type ButtonType = 'button' | 'submit' | 'reset';\n\n/** Icon pixel size per button size (per design: sm/md 16, lg 20, xl 24). */\nconst ICON_SIZE_BY_BUTTON_SIZE: Record<ButtonSize, number> = {\n small: 16,\n medium: 16,\n large: 20,\n xlarge: 24,\n};\n\n/**\n * A themeable button that renders a native `<button>` element, so it is fully\n * form-submittable and keyboard/screen-reader accessible out of the box. Icons\n * are Lucide glyphs (matching the rest of the library); a button may carry a\n * leading icon, a trailing icon, or both.\n */\n@Component({\n selector: 'compsych-button',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <button\n [class]=\"buttonClasses\"\n [type]=\"type\"\n [disabled]=\"disabled || loading\"\n [attr.aria-busy]=\"loading ? 'true' : null\"\n [attr.aria-label]=\"computedAriaLabel\"\n (pointerdown)=\"onPointerDown($event)\"\n (keydown)=\"onKeyDown($event)\"\n >\n @if (loading) {\n <span class=\"icon spinner\" aria-hidden=\"true\"></span>\n } @else if (leadingIcon) {\n <span class=\"icon\" aria-hidden=\"true\" [innerHTML]=\"leadingIconSvg\"></span>\n }\n @if (!iconOnly) {\n <span class=\"label\">{{ label }}<ng-content /></span>\n }\n @if (trailingIcon && !iconOnly) {\n <span class=\"icon\" aria-hidden=\"true\" [innerHTML]=\"trailingIconSvg\"></span>\n }\n </button>\n `,\n styleUrl: './button.css',\n})\nexport class ButtonComponent implements OnChanges {\n private readonly sanitizer = inject(DomSanitizer);\n\n /** Text label rendered on the button. */\n @Input() label = '';\n /** Visual style of the button. Defaults to `'filled'`. */\n @Input() variant: ButtonVariant = 'filled';\n /** Button dimensions — controls height, padding, font size, and icon size. Defaults to `'small'`. */\n @Input() size: ButtonSize = 'small';\n /** Lucide icon name shown before the label (e.g. `'house'`). Swapped for a spinner when loading. */\n @Input() leadingIcon?: string;\n /** Lucide icon name shown after the label (e.g. `'chevron-right'`). Can be combined with `leadingIcon`. */\n @Input() trailingIcon?: string;\n /** Native button type. Use `'submit'` inside a `<form>` to submit it. Defaults to `'button'`. */\n @Input() type: ButtonType = 'button';\n /**\n * Accessible name. Required for icon-only buttons since the icon is decorative.\n * When `iconOnly` or `loading` is set and this is omitted, `label` is used.\n */\n @Input() ariaLabel?: string;\n\n /** Hides the label and renders a square icon-only button. Uses `leadingIcon`. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) iconOnly = false;\n /** Disables the button — non-interactive and rendered in a neutral grey. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) disabled = false;\n /**\n * Loading state. Swaps the leading icon for a spinner and suppresses activation\n * (native `disabled` blocks click/keyboard), while keeping the label and exposing\n * `aria-busy=\"true\"`. Retains the variant's colours. Supports boolean coercion.\n */\n @Input({ transform: booleanAttribute }) loading = false;\n /** Stretches the button to the full width of its container. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) fullWidth = false;\n\n /** Mirrors `fullWidth` onto the host so it can stretch. */\n @HostBinding('class.full-width') get hostFullWidth(): boolean {\n return this.fullWidth;\n }\n\n /** Pre-sanitized icon markup, recomputed on input change (avoids re-sanitizing every CD cycle). */\n protected leadingIconSvg: SafeHtml | null = null;\n protected trailingIconSvg: SafeHtml | null = null;\n\n ngOnChanges(): void {\n this.leadingIconSvg = this.leadingIcon ? this.buildIconSvg(this.leadingIcon) : null;\n this.trailingIconSvg = this.trailingIcon ? this.buildIconSvg(this.trailingIcon) : null;\n }\n\n /** Space-separated class list applied to the inner native button. */\n get buttonClasses(): string {\n return [\n 'cmp-btn',\n this.variant,\n this.size,\n this.iconOnly ? 'icon-only' : '',\n this.loading ? 'loading' : '',\n this.fullWidth ? 'full-width' : '',\n ]\n .filter(Boolean)\n .join(' ');\n }\n\n /** Resolves the accessible name. Icon-only buttons have no visible text, so they fall back to `label`. */\n get computedAriaLabel(): string | null {\n if (this.ariaLabel) {\n return this.ariaLabel;\n }\n // Loading keeps the visible label, so only icon-only needs an explicit name here.\n return this.iconOnly ? this.label || null : null;\n }\n\n /**\n * Looks up a Lucide icon by name and injects an explicit width/height so the SVG\n * renders at the correct size when injected via `[innerHTML]` (Angular's CSS\n * encapsulation does not reach dynamically injected content).\n */\n private buildIconSvg(name: string): SafeHtml {\n const size = ICON_SIZE_BY_BUTTON_SIZE[this.size];\n const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\n return this.sanitizer.bypassSecurityTrustHtml(svg);\n }\n\n /** M3 pressed ripple: pointer-down spawns a ripple at the pointer position. */\n onPointerDown(event: PointerEvent): void {\n if (this.disabled || this.loading || event.button !== 0) {\n return;\n }\n const host = event.currentTarget as HTMLElement;\n const rect = host.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n this.spawnRipple(host, x, y, this.farthestCornerDiameter(rect, x, y));\n }\n\n /** M3 pressed ripple: keyboard activation (Enter/Space) spawns a centered ripple. */\n onKeyDown(event: KeyboardEvent): void {\n if (this.disabled || this.loading || event.repeat) {\n return;\n }\n if (event.key !== 'Enter' && event.key !== ' ') {\n return;\n }\n const host = event.currentTarget as HTMLElement;\n const rect = host.getBoundingClientRect();\n this.spawnRipple(host, rect.width / 2, rect.height / 2, Math.hypot(rect.width, rect.height));\n }\n\n /** Diameter that reaches the farthest corner from (x, y), so the ripple always covers the button. */\n private farthestCornerDiameter(rect: DOMRect, x: number, y: number): number {\n const corners: Array<[number, number]> = [\n [0, 0],\n [rect.width, 0],\n [0, rect.height],\n [rect.width, rect.height],\n ];\n const maxRadius = Math.max(...corners.map(([cx, cy]) => Math.hypot(cx - x, cy - y)));\n return maxRadius * 2;\n }\n\n /**\n * Spawns an expanding ripple circle. Created imperatively with inline styles and\n * animated via the Web Animations API — Angular's emulated encapsulation does not\n * reach dynamically created nodes, so it cannot rely on the component stylesheet.\n */\n private spawnRipple(host: HTMLElement, x: number, y: number, diameter: number): void {\n if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {\n return;\n }\n const ripple = document.createElement('span');\n Object.assign(ripple.style, {\n position: 'absolute',\n borderRadius: '50%',\n backgroundColor: 'currentColor',\n pointerEvents: 'none',\n width: `${diameter}px`,\n height: `${diameter}px`,\n left: `${x - diameter / 2}px`,\n top: `${y - diameter / 2}px`,\n } as Partial<CSSStyleDeclaration>);\n host.appendChild(ripple);\n const animation = ripple.animate(\n [\n { transform: 'scale(0)', opacity: '0.16' },\n { transform: 'scale(1)', opacity: '0' },\n ],\n { duration: 450, easing: 'ease-out' },\n );\n animation.onfinish = () => ripple.remove();\n }\n}\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) {\r\n <nav class=\"compsych-footer__links\" aria-label=\"Footer navigation\">\r\n <ul role=\"list\">\r\n @for (link of links; track link.href; let first = $first) {\r\n <li>\r\n @if (!first) {\r\n <span class=\"compsych-footer__divider\" aria-hidden=\"true\"></span>\r\n }\r\n <a class=\"compsych-footer__link\" [href]=\"link.href\">{{ link.label }}</a>\r\n </li>\r\n }\r\n </ul>\r\n </nav>\r\n}\r\n\r\n<p class=\"compsych-footer__copyright\">{{ copyright }}</p>\r\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) {\r\n <nav class=\"compsych-footer__links\" aria-label=\"Footer navigation\">\r\n <ul role=\"list\">\r\n @for (link of links; track link.href; let first = $first) {\r\n <li>\r\n @if (!first) {\r\n <span class=\"compsych-footer__divider\" aria-hidden=\"true\"></span>\r\n }\r\n <a class=\"compsych-footer__link\" [href]=\"link.href\">{{ link.label }}</a>\r\n </li>\r\n }\r\n </ul>\r\n </nav>\r\n}\r\n\r\n<p class=\"compsych-footer__copyright\">{{ copyright }}</p>\r\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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/footer/footer.ts","../../../../../../packages/angular/src/lib/footer/footer.html"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,SAAS,EAET,YAAY,EACZ,KAAK,EAEL,eAAe,EACf,MAAM,GACP,MAAM,eAAe,CAAC;;AAUvB;;;;;;;;GAQG;AAaH,MAAM,OAAO,eAAe;IAsBG;IArB7B;;;OAGG;IACM,KAAK,GAAiB,EAAE,CAAC;IAElC;;;OAGG;IACM,SAAS,GAAG,oBAAoB,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,uCAAuC,CAAC;IAEzG;;;OAGG;IACgB,KAAK,GAAG,MAAM,CAAC,KAAK,4EAAC,CAAC;IAEzC,mEAAmE;IAC3D,UAAU,GAAG,CAAC,CAAC;IAEvB,YAA6B,EAA2B;QAA3B,OAAE,GAAF,EAAE,CAAyB;QACtD,eAAe,CAAC,GAAG,EAAE;YACnB,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,WAAW;QACT,6EAA6E;QAC7E,qBAAqB,CAAC,GAAG,EAAE;YACzB,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IAGS,QAAQ;QAChB,IAAI,CAAC,WAAW,EAAE,CAAC;IACrB,CAAC;IAED;;;OAGG;IACK,iBAAiB;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC;QACnC,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAc,6BAA6B,CAAC,CAAC;QACnF,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAc,yBAAyB,CAAC,CAAC;QAC3E,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzB,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,aAAa,GAAG,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACrF,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAE7C,IAAI,CAAC,UAAU;YACb,WAAW,CAAC,WAAW;gBACvB,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACzC,aAAa,CAAC;IAClB,CAAC;IAED,iFAAiF;IACzE,WAAW;QACjB,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO;QAC7B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,WAAW,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC;IACvE,CAAC;wGAlEU,eAAe;4FAAf,eAAe,oRCxC5B,6iBAgBA;;4FDwBa,eAAe;kBAZ3B,SAAS;+BACE,iBAAiB,cACf,IAAI,mBACC,uBAAuB,CAAC,MAAM,WACtC,EAAE,QAGL;wBACJ,IAAI,EAAE,aAAa;wBACnB,8BAA8B,EAAE,SAAS;qBAC1C;;sBAOA,KAAK;;sBAgCL,YAAY;uBAAC,eAAe","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n ElementRef,\r\n HostListener,\r\n Input,\r\n OnChanges,\r\n afterNextRender,\r\n signal,\r\n} from '@angular/core';\r\n\r\n/** A single navigation link in the footer. */\r\nexport interface FooterLink {\r\n /** Text displayed in the link. */\r\n label: string;\r\n /** URL the link navigates to. */\r\n href: string;\r\n}\r\n\r\n/**\r\n * Footer — bottom bar from the ComPsych Design System.\r\n *\r\n * Displays a copyright notice and an optional list of navigation links.\r\n * On initialisation the component measures the natural widths of the copyright\r\n * and links to establish a breakpoint. On every window resize it compares the\r\n * host width against that breakpoint and switches between a row layout\r\n * (copyright left, links right) and a stacked, centered column layout.\r\n */\r\n@Component({\r\n selector: 'compsych-footer',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [],\r\n templateUrl: './footer.html',\r\n styleUrl: './footer.css',\r\n host: {\r\n role: 'contentinfo',\r\n '[class.compsych-footer--row]': 'isRow()',\r\n },\r\n})\r\nexport class FooterComponent implements OnChanges {\r\n /**\r\n * Navigation links rendered alongside the copyright notice.\r\n * The layout switches automatically based on their combined natural width.\r\n */\r\n @Input() links: FooterLink[] = [];\r\n\r\n /**\r\n * Copyright notice generated from the current calendar year.\r\n * Left-aligned in the row layout and centered when stacked.\r\n */\r\n readonly copyright = `Copyright \\u00a9 ${new Date().getFullYear()} ComPsych\\u00ae. All rights reserved.`;\r\n\r\n /**\r\n * Reflects whether the row layout is active.\r\n * Signal so OnPush picks up DOM-driven updates from async callbacks.\r\n */\r\n protected readonly isRow = signal(false);\r\n\r\n /** Combined natural width of copyright + links + gap + padding. */\r\n private breakpoint = 0;\r\n\r\n constructor(private readonly el: ElementRef<HTMLElement>) {\r\n afterNextRender(() => {\r\n this.measureBreakpoint();\r\n this.checkLayout();\r\n });\r\n }\r\n\r\n ngOnChanges(): void {\r\n // Wait for Angular to update the DOM with the new links before re-measuring.\r\n requestAnimationFrame(() => {\r\n this.measureBreakpoint();\r\n this.checkLayout();\r\n });\r\n }\r\n\r\n @HostListener('window:resize')\r\n protected onResize(): void {\r\n this.checkLayout();\r\n }\r\n\r\n /**\r\n * Measures the natural widths of the copyright and links elements once and\r\n * stores their sum (plus padding and gap) as the layout breakpoint.\r\n */\r\n private measureBreakpoint(): void {\r\n const host = this.el.nativeElement;\r\n const copyrightEl = host.querySelector<HTMLElement>('.compsych-footer__copyright');\r\n const linksEl = host.querySelector<HTMLElement>('.compsych-footer__links');\r\n if (!copyrightEl) return;\r\n\r\n const style = getComputedStyle(host);\r\n const paddingInline = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);\r\n const gap = parseFloat(style.columnGap) || 0;\r\n\r\n this.breakpoint =\r\n copyrightEl.scrollWidth +\r\n (linksEl ? linksEl.scrollWidth + gap : 0) +\r\n paddingInline;\r\n }\r\n\r\n /** Switches to the row layout when the host is wide enough for both children. */\r\n private checkLayout(): void {\r\n if (!this.breakpoint) return;\r\n this.isRow.set(this.el.nativeElement.offsetWidth >= this.breakpoint);\r\n }\r\n}\r\n","@if (links.length) {\r\n <nav class=\"compsych-footer__links\" aria-label=\"Footer navigation\">\r\n <ul role=\"list\">\r\n @for (link of links; track link.href; let first = $first) {\r\n <li>\r\n @if (!first) {\r\n <span class=\"compsych-footer__divider\" aria-hidden=\"true\"></span>\r\n }\r\n <a class=\"compsych-footer__link\" [href]=\"link.href\">{{ link.label }}</a>\r\n </li>\r\n }\r\n </ul>\r\n </nav>\r\n}\r\n\r\n<p class=\"compsych-footer__copyright\">{{ copyright }}</p>\r\n"]}
package/index.d.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import * as i1 from "./lib/button/button";
3
3
  import * as i2 from "./lib/accordion/accordion";
4
- import * as i3 from "./lib/header-nav/header-nav";
5
- import * as i4 from "./lib/sidebar/sidebar";
4
+ import * as i3 from "./lib/footer/footer";
5
+ import * as i4 from "./lib/header-nav/header-nav";
6
+ import * as i5 from "./lib/sidebar/sidebar";
6
7
  export { ButtonComponent } from './lib/button/button';
7
8
  export type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';
8
9
  export { AccordionComponent } from './lib/accordion/accordion';
9
10
  export type { AccordionItem, AccordionButton, AccordionVariant, NestedAccordionItem } from './lib/accordion/accordion-types';
11
+ export { FooterComponent } from './lib/footer/footer';
12
+ export type { FooterLink } from './lib/footer/footer';
10
13
  export { HeaderNavComponent } from './lib/header-nav/header-nav';
11
14
  export type { HeaderNavLevel, NavAction } from './lib/header-nav/header-nav';
12
15
  export { SidebarComponent } from './lib/sidebar/sidebar';
13
16
  export type { SidebarItem } from './lib/sidebar/sidebar';
14
17
  export declare class CompsychAngularModule {
15
18
  static ɵfac: i0.ɵɵFactoryDeclaration<CompsychAngularModule, never>;
16
- static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.HeaderNavComponent, typeof i4.SidebarComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.HeaderNavComponent, typeof i4.SidebarComponent]>;
19
+ static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent]>;
17
20
  static ɵinj: i0.ɵɵInjectorDeclaration<CompsychAngularModule>;
18
21
  }
@@ -0,0 +1,50 @@
1
+ import { ElementRef, OnChanges } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ /** A single navigation link in the footer. */
4
+ export interface FooterLink {
5
+ /** Text displayed in the link. */
6
+ label: string;
7
+ /** URL the link navigates to. */
8
+ href: string;
9
+ }
10
+ /**
11
+ * Footer — bottom bar from the ComPsych Design System.
12
+ *
13
+ * Displays a copyright notice and an optional list of navigation links.
14
+ * On initialisation the component measures the natural widths of the copyright
15
+ * and links to establish a breakpoint. On every window resize it compares the
16
+ * host width against that breakpoint and switches between a row layout
17
+ * (copyright left, links right) and a stacked, centered column layout.
18
+ */
19
+ export declare class FooterComponent implements OnChanges {
20
+ private readonly el;
21
+ /**
22
+ * Navigation links rendered alongside the copyright notice.
23
+ * The layout switches automatically based on their combined natural width.
24
+ */
25
+ links: FooterLink[];
26
+ /**
27
+ * Copyright notice generated from the current calendar year.
28
+ * Left-aligned in the row layout and centered when stacked.
29
+ */
30
+ readonly copyright: string;
31
+ /**
32
+ * Reflects whether the row layout is active.
33
+ * Signal so OnPush picks up DOM-driven updates from async callbacks.
34
+ */
35
+ protected readonly isRow: import("@angular/core").WritableSignal<boolean>;
36
+ /** Combined natural width of copyright + links + gap + padding. */
37
+ private breakpoint;
38
+ constructor(el: ElementRef<HTMLElement>);
39
+ ngOnChanges(): void;
40
+ protected onResize(): void;
41
+ /**
42
+ * Measures the natural widths of the copyright and links elements once and
43
+ * stores their sum (plus padding and gap) as the layout breakpoint.
44
+ */
45
+ private measureBreakpoint;
46
+ /** Switches to the row layout when the host is wide enough for both children. */
47
+ private checkLayout;
48
+ static ɵfac: i0.ɵɵFactoryDeclaration<FooterComponent, never>;
49
+ static ɵcmp: i0.ɵɵComponentDeclaration<FooterComponent, "compsych-footer", never, { "links": { "alias": "links"; "required": false; }; }, {}, never, never, true, never>;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compsych-ui-components/angular",
3
- "version": "7.0.0",
3
+ "version": "7.0.2",
4
4
  "description": "Angular UI components for compsych",
5
5
  "publishConfig": {
6
6
  "access": "public",