@compsych-ui-components/angular 6.1.2 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/index.js +3 -5
- package/esm2022/index.js.map +1 -1
- package/esm2022/lib/button/button.js +44 -44
- package/esm2022/lib/button/button.js.map +1 -1
- package/esm2022/lib/sidebar/sidebar.js +2 -2
- package/esm2022/lib/sidebar/sidebar.js.map +1 -1
- package/index.d.ts +3 -6
- package/lib/sidebar/sidebar.d.ts +2 -2
- package/package.json +1 -1
- package/esm2022/lib/footer/footer.js +0 -87
- package/esm2022/lib/footer/footer.js.map +0 -1
- package/lib/footer/footer.d.ts +0 -50
package/esm2022/index.js
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
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';
|
|
5
4
|
import { HeaderNavComponent } from './lib/header-nav/header-nav';
|
|
6
5
|
import { SidebarComponent } from './lib/sidebar/sidebar';
|
|
7
6
|
import * as i0 from "@angular/core";
|
|
8
7
|
export { ButtonComponent } from './lib/button/button';
|
|
9
8
|
export { AccordionComponent } from './lib/accordion/accordion';
|
|
10
|
-
export { FooterComponent } from './lib/footer/footer';
|
|
11
9
|
export { HeaderNavComponent } from './lib/header-nav/header-nav';
|
|
12
10
|
export { SidebarComponent } from './lib/sidebar/sidebar';
|
|
13
11
|
export class CompsychAngularModule {
|
|
14
12
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
15
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, imports: [ButtonComponent, AccordionComponent,
|
|
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] });
|
|
16
14
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule });
|
|
17
15
|
}
|
|
18
16
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, decorators: [{
|
|
19
17
|
type: NgModule,
|
|
20
18
|
args: [{
|
|
21
|
-
imports: [ButtonComponent, AccordionComponent,
|
|
22
|
-
exports: [ButtonComponent, AccordionComponent,
|
|
19
|
+
imports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent],
|
|
20
|
+
exports: [ButtonComponent, AccordionComponent, HeaderNavComponent, SidebarComponent],
|
|
23
21
|
}]
|
|
24
22
|
}] });
|
|
25
23
|
//# sourceMappingURL=index.js.map
|
package/esm2022/index.js.map
CHANGED
|
@@ -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,
|
|
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"]}
|
|
@@ -150,54 +150,54 @@ 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>
|
|
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
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 });
|
|
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>
|
|
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
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"] }]
|
|
202
202
|
}], propDecorators: { label: [{
|
|
203
203
|
type: Input
|
|
@@ -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 {\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"]}
|
|
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"]}
|
|
@@ -48,8 +48,8 @@ export class SidebarComponent {
|
|
|
48
48
|
/** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */
|
|
49
49
|
activeId = model('home', ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
|
|
50
50
|
/**
|
|
51
|
-
* Emitted with the item's `id`
|
|
52
|
-
*
|
|
51
|
+
* Emitted with the item's `id` when a non-disabled action (`nonRoute`) item is clicked.
|
|
52
|
+
* Navigation (route) items do not emit — they update `activeId` instead (bind via `[(activeId)]`).
|
|
53
53
|
* Group items (those with `children`) do not emit — they only toggle their expanded state.
|
|
54
54
|
*/
|
|
55
55
|
itemClick = output();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/sidebar/sidebar.ts","../../../../../../packages/angular/src/lib/sidebar/sidebar.html"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n model,\r\n input,\r\n output,\r\n signal,\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\nexport interface SidebarItem {\r\n /** Stable identifier used by `activeId` and emitted by `itemClick`. */\r\n id: string;\r\n /** Text label displayed next to the icon. */\r\n label: string;\r\n /** Lucide icon name (kebab-case), e.g. `'house'` or `'folder'`. */\r\n icon: string;\r\n /** Child items — renders this item as an expandable group with a chevron toggle. Child items cannot have their own children. */\r\n children?: SidebarItem[];\r\n /** When true, renders a horizontal divider above this item in the list. */\r\n dividerBefore?: boolean;\r\n /** When true, the item is non-interactive — it cannot be clicked and receives no hover or active styling. */\r\n disabled?: boolean;\r\n /**\r\n * When true, clicking the item emits `itemClick` but does not highlight it\r\n * as active or update `activeId`. Use for action items (e.g. logout)\r\n * that are not navigation destinations.\r\n */\r\n nonRoute?: boolean;\r\n}\r\n\r\n/** Default content from the Figma (node 260:4700). */\r\nconst DEFAULT_ITEMS: SidebarItem[] = [\r\n { id: 'home', label: 'Home', icon: 'house' },\r\n { id: 'connect', label: 'Connect Me', icon: 'message-circle', dividerBefore: true },\r\n { id: 'guide', label: 'Guide Me', icon: 'compass' },\r\n { id: 'assess', label: 'Assess Me', icon: 'binoculars' },\r\n {\r\n id: 'resources',\r\n label: 'Resource Library',\r\n icon: 'folder',\r\n dividerBefore: true,\r\n children: [\r\n { id: 'wellness', label: 'Wellness', icon: 'leaf' },\r\n { id: 'relationships', label: 'Relationships', icon: 'users' },\r\n { id: 'work', label: 'Work & Education', icon: 'briefcase' },\r\n { id: 'financial', label: 'Financial', icon: 'landmark' },\r\n { id: 'legal', label: 'Legal', icon: 'scale' },\r\n { id: 'lifestyle', label: 'Lifestyle', icon: 'smile' },\r\n { id: 'home-auto', label: 'Home & Auto', icon: 'car' },\r\n { id: 'abroad', label: 'Working Abroad', icon: 'plane' },\r\n ],\r\n },\r\n];\r\n\r\nconst DEFAULT_FOOTER: SidebarItem[] = [\r\n { id: 'location', label: 'Location & Language', icon: 'globe', nonRoute: true },\r\n { id: 'logout', label: 'Logout', icon: 'log-out', nonRoute: true },\r\n];\r\n\r\n/**\r\n * Sidebar — vertical app navigation from the ComPsych Design System.\r\n *\r\n * Figma: Core Components — base 260:4700, parts 213:1585.\r\n *\r\n * Icon + label items with default / hover / selected / focus / disabled states,\r\n * section dividers, expandable groups (chevron), and a pinned footer\r\n * (location & language, logout). Data-driven via `items` / `footerItems`,\r\n * defaulting to the design's content. All values are sys tokens.\r\n */\r\n@Component({\r\n selector: 'compsych-sidebar',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [],\r\n templateUrl: './sidebar.html',\r\n styleUrl: './sidebar.css',\r\n host: { role: 'navigation', 'aria-label': 'Sidebar' },\r\n})\r\nexport class SidebarComponent {\r\n /** Navigation items rendered in the scrollable main area. Defaults to the standard ComPsych nav set. */\r\n readonly items = input<SidebarItem[]>(DEFAULT_ITEMS);\r\n /** Items pinned to the bottom of the sidebar, outside the scroll region. Intended for utility actions such as language selection and logout. */\r\n readonly footerItems = input<SidebarItem[]>(DEFAULT_FOOTER);\r\n /** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */\r\n readonly activeId = model<string>('home');\r\n /**\r\n * Emitted with the item's `id` whenever a non-disabled, non-group item is clicked.\r\n * Fires for both route items and `nonRoute` items.\r\n * Group items (those with `children`) do not emit — they only toggle their expanded state.\r\n */\r\n readonly itemClick = output<string>();\r\n\r\n private readonly expanded = signal<Set<string>>(new Set());\r\n\r\n constructor(private readonly sanitizer: DomSanitizer) {}\r\n\r\n isExpanded(id: string): boolean {\r\n return this.expanded().has(id);\r\n }\r\n\r\n isActive(id: string): boolean {\r\n return this.activeId() === id;\r\n }\r\n\r\n onItemClick(item: SidebarItem): void {\r\n if (item.disabled) return;\r\n if (item.children?.length) {\r\n this.expanded.update((s) => {\r\n const next = new Set(s);\r\n next.has(item.id) ? next.delete(item.id) : next.add(item.id);\r\n return next;\r\n });\r\n return;\r\n }\r\n if (item.nonRoute) {\r\n this.itemClick.emit(item.id);\r\n return;\r\n }\r\n this.activeId.set(item.id);\r\n }\r\n\r\n /**\r\n * Looks up a Lucide icon by name and injects explicit width/height attributes\r\n * so the SVG renders at the correct size when injected via [innerHTML].\r\n * Angular's CSS encapsulation does not add _ngcontent attributes to dynamically\r\n * injected content, so explicit SVG attributes are the reliable solution.\r\n */\r\n iconSvg(name: string, size = 20, strokeWidth = 1.5): SafeHtml {\r\n let svg = LUCIDE_ICONS[name] ?? '';\r\n svg = svg.replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n if (strokeWidth !== 1.5) {\r\n svg = svg.replace('stroke-width=\"1.5\"', `stroke-width=\"${strokeWidth}\"`);\r\n }\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n}\r\n","<div class=\"cs-sb__scroll\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of items(); track item.id) {\r\n @if (item.dividerBefore) {\r\n <li class=\"cs-sb__divider\" aria-hidden=\"true\"></li>\r\n }\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [disabled]=\"item.disabled || null\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item.id) : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n @if (item.children?.length) {\r\n <span\r\n class=\"cs-sb__chevron\"\r\n [class.cs-sb__chevron--open]=\"isExpanded(item.id)\"\r\n [innerHTML]=\"iconSvg('chevron-down', 18)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n }\r\n </button>\r\n\r\n @if (item.children?.length && isExpanded(item.id)) {\r\n <ul class=\"cs-sb__sublist\">\r\n @for (child of item.children; track child.id) {\r\n <li>\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row cs-sb__row--sub\"\r\n [class.cs-sb__row--active]=\"isActive(child.id)\"\r\n [disabled]=\"child.disabled || null\"\r\n [attr.aria-current]=\"isActive(child.id) ? 'page' : null\"\r\n (click)=\"onItemClick(child)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(child.icon, 20, isActive(child.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ child.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n\r\n<div class=\"cs-sb__footer\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of footerItems(); track item.id) {\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [disabled]=\"item.disabled || null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n"]}
|
|
1
|
+
{"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/sidebar/sidebar.ts","../../../../../../packages/angular/src/lib/sidebar/sidebar.html"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n model,\r\n input,\r\n output,\r\n signal,\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\nexport interface SidebarItem {\r\n /** Stable identifier used by `activeId` and emitted by `itemClick`. */\r\n id: string;\r\n /** Text label displayed next to the icon. */\r\n label: string;\r\n /** Lucide icon name (kebab-case), e.g. `'house'` or `'folder'`. */\r\n icon: string;\r\n /** Child items — renders this item as an expandable group with a chevron toggle. Child items cannot have their own children. */\r\n children?: SidebarItem[];\r\n /** When true, renders a horizontal divider above this item in the list. */\r\n dividerBefore?: boolean;\r\n /** When true, the item is non-interactive — it cannot be clicked and receives no hover or active styling. */\r\n disabled?: boolean;\r\n /**\r\n * When true, clicking the item emits `itemClick` but does not highlight it\r\n * as active or update `activeId`. Use for action items (e.g. logout)\r\n * that are not navigation destinations.\r\n */\r\n nonRoute?: boolean;\r\n}\r\n\r\n/** Default content from the Figma (node 260:4700). */\r\nconst DEFAULT_ITEMS: SidebarItem[] = [\r\n { id: 'home', label: 'Home', icon: 'house' },\r\n { id: 'connect', label: 'Connect Me', icon: 'message-circle', dividerBefore: true },\r\n { id: 'guide', label: 'Guide Me', icon: 'compass' },\r\n { id: 'assess', label: 'Assess Me', icon: 'binoculars' },\r\n {\r\n id: 'resources',\r\n label: 'Resource Library',\r\n icon: 'folder',\r\n dividerBefore: true,\r\n children: [\r\n { id: 'wellness', label: 'Wellness', icon: 'leaf' },\r\n { id: 'relationships', label: 'Relationships', icon: 'users' },\r\n { id: 'work', label: 'Work & Education', icon: 'briefcase' },\r\n { id: 'financial', label: 'Financial', icon: 'landmark' },\r\n { id: 'legal', label: 'Legal', icon: 'scale' },\r\n { id: 'lifestyle', label: 'Lifestyle', icon: 'smile' },\r\n { id: 'home-auto', label: 'Home & Auto', icon: 'car' },\r\n { id: 'abroad', label: 'Working Abroad', icon: 'plane' },\r\n ],\r\n },\r\n];\r\n\r\nconst DEFAULT_FOOTER: SidebarItem[] = [\r\n { id: 'location', label: 'Location & Language', icon: 'globe', nonRoute: true },\r\n { id: 'logout', label: 'Logout', icon: 'log-out', nonRoute: true },\r\n];\r\n\r\n/**\r\n * Sidebar — vertical app navigation from the ComPsych Design System.\r\n *\r\n * Figma: Core Components — base 260:4700, parts 213:1585.\r\n *\r\n * Icon + label items with default / hover / selected / focus / disabled states,\r\n * section dividers, expandable groups (chevron), and a pinned footer\r\n * (location & language, logout). Data-driven via `items` / `footerItems`,\r\n * defaulting to the design's content. All values are sys tokens.\r\n */\r\n@Component({\r\n selector: 'compsych-sidebar',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [],\r\n templateUrl: './sidebar.html',\r\n styleUrl: './sidebar.css',\r\n host: { role: 'navigation', 'aria-label': 'Sidebar' },\r\n})\r\nexport class SidebarComponent {\r\n /** Navigation items rendered in the scrollable main area. Defaults to the standard ComPsych nav set. */\r\n readonly items = input<SidebarItem[]>(DEFAULT_ITEMS);\r\n /** Items pinned to the bottom of the sidebar, outside the scroll region. Intended for utility actions such as language selection and logout. */\r\n readonly footerItems = input<SidebarItem[]>(DEFAULT_FOOTER);\r\n /** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */\r\n readonly activeId = model<string>('home');\r\n /**\r\n * Emitted with the item's `id` when a non-disabled action (`nonRoute`) item is clicked.\r\n * Navigation (route) items do not emit — they update `activeId` instead (bind via `[(activeId)]`).\r\n * Group items (those with `children`) do not emit — they only toggle their expanded state.\r\n */\r\n readonly itemClick = output<string>();\r\n\r\n private readonly expanded = signal<Set<string>>(new Set());\r\n\r\n constructor(private readonly sanitizer: DomSanitizer) {}\r\n\r\n isExpanded(id: string): boolean {\r\n return this.expanded().has(id);\r\n }\r\n\r\n isActive(id: string): boolean {\r\n return this.activeId() === id;\r\n }\r\n\r\n onItemClick(item: SidebarItem): void {\r\n if (item.disabled) return;\r\n if (item.children?.length) {\r\n this.expanded.update((s) => {\r\n const next = new Set(s);\r\n next.has(item.id) ? next.delete(item.id) : next.add(item.id);\r\n return next;\r\n });\r\n return;\r\n }\r\n if (item.nonRoute) {\r\n this.itemClick.emit(item.id);\r\n return;\r\n }\r\n this.activeId.set(item.id);\r\n }\r\n\r\n /**\r\n * Looks up a Lucide icon by name and injects explicit width/height attributes\r\n * so the SVG renders at the correct size when injected via [innerHTML].\r\n * Angular's CSS encapsulation does not add _ngcontent attributes to dynamically\r\n * injected content, so explicit SVG attributes are the reliable solution.\r\n */\r\n iconSvg(name: string, size = 20, strokeWidth = 1.5): SafeHtml {\r\n let svg = LUCIDE_ICONS[name] ?? '';\r\n svg = svg.replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n if (strokeWidth !== 1.5) {\r\n svg = svg.replace('stroke-width=\"1.5\"', `stroke-width=\"${strokeWidth}\"`);\r\n }\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n}\r\n","<div class=\"cs-sb__scroll\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of items(); track item.id) {\r\n @if (item.dividerBefore) {\r\n <li class=\"cs-sb__divider\" aria-hidden=\"true\"></li>\r\n }\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [disabled]=\"item.disabled || null\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item.id) : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n @if (item.children?.length) {\r\n <span\r\n class=\"cs-sb__chevron\"\r\n [class.cs-sb__chevron--open]=\"isExpanded(item.id)\"\r\n [innerHTML]=\"iconSvg('chevron-down', 18)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n }\r\n </button>\r\n\r\n @if (item.children?.length && isExpanded(item.id)) {\r\n <ul class=\"cs-sb__sublist\">\r\n @for (child of item.children; track child.id) {\r\n <li>\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row cs-sb__row--sub\"\r\n [class.cs-sb__row--active]=\"isActive(child.id)\"\r\n [disabled]=\"child.disabled || null\"\r\n [attr.aria-current]=\"isActive(child.id) ? 'page' : null\"\r\n (click)=\"onItemClick(child)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(child.icon, 20, isActive(child.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ child.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n\r\n<div class=\"cs-sb__footer\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of footerItems(); track item.id) {\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [disabled]=\"item.disabled || null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n"]}
|
package/index.d.ts
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
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/
|
|
5
|
-
import * as i4 from "./lib/
|
|
6
|
-
import * as i5 from "./lib/sidebar/sidebar";
|
|
4
|
+
import * as i3 from "./lib/header-nav/header-nav";
|
|
5
|
+
import * as i4 from "./lib/sidebar/sidebar";
|
|
7
6
|
export { ButtonComponent } from './lib/button/button';
|
|
8
7
|
export type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';
|
|
9
8
|
export { AccordionComponent } from './lib/accordion/accordion';
|
|
10
9
|
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';
|
|
13
10
|
export { HeaderNavComponent } from './lib/header-nav/header-nav';
|
|
14
11
|
export type { HeaderNavLevel, NavAction } from './lib/header-nav/header-nav';
|
|
15
12
|
export { SidebarComponent } from './lib/sidebar/sidebar';
|
|
16
13
|
export type { SidebarItem } from './lib/sidebar/sidebar';
|
|
17
14
|
export declare class CompsychAngularModule {
|
|
18
15
|
static ɵfac: i0.ɵɵFactoryDeclaration<CompsychAngularModule, never>;
|
|
19
|
-
static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.
|
|
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]>;
|
|
20
17
|
static ɵinj: i0.ɵɵInjectorDeclaration<CompsychAngularModule>;
|
|
21
18
|
}
|
package/lib/sidebar/sidebar.d.ts
CHANGED
|
@@ -39,8 +39,8 @@ export declare class SidebarComponent {
|
|
|
39
39
|
/** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */
|
|
40
40
|
readonly activeId: import("@angular/core").ModelSignal<string>;
|
|
41
41
|
/**
|
|
42
|
-
* Emitted with the item's `id`
|
|
43
|
-
*
|
|
42
|
+
* Emitted with the item's `id` when a non-disabled action (`nonRoute`) item is clicked.
|
|
43
|
+
* Navigation (route) items do not emit — they update `activeId` instead (bind via `[(activeId)]`).
|
|
44
44
|
* Group items (those with `children`) do not emit — they only toggle their expanded state.
|
|
45
45
|
*/
|
|
46
46
|
readonly itemClick: import("@angular/core").OutputEmitterRef<string>;
|
package/package.json
CHANGED
|
@@ -1,87 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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,6gBAgBA;;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 {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n HostListener,\n Input,\n OnChanges,\n afterNextRender,\n signal,\n} from '@angular/core';\n\n/** A single navigation link in the footer. */\nexport interface FooterLink {\n /** Text displayed in the link. */\n label: string;\n /** URL the link navigates to. */\n href: string;\n}\n\n/**\n * Footer — bottom bar from the ComPsych Design System.\n *\n * Displays a copyright notice and an optional list of navigation links.\n * On initialisation the component measures the natural widths of the copyright\n * and links to establish a breakpoint. On every window resize it compares the\n * host width against that breakpoint and switches between a row layout\n * (copyright left, links right) and a stacked, centered column layout.\n */\n@Component({\n selector: 'compsych-footer',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [],\n templateUrl: './footer.html',\n styleUrl: './footer.css',\n host: {\n role: 'contentinfo',\n '[class.compsych-footer--row]': 'isRow()',\n },\n})\nexport class FooterComponent implements OnChanges {\n /**\n * Navigation links rendered alongside the copyright notice.\n * The layout switches automatically based on their combined natural width.\n */\n @Input() links: FooterLink[] = [];\n\n /**\n * Copyright notice generated from the current calendar year.\n * Left-aligned in the row layout and centered when stacked.\n */\n readonly copyright = `Copyright \\u00a9 ${new Date().getFullYear()} ComPsych\\u00ae. All rights reserved.`;\n\n /**\n * Reflects whether the row layout is active.\n * Signal so OnPush picks up DOM-driven updates from async callbacks.\n */\n protected readonly isRow = signal(false);\n\n /** Combined natural width of copyright + links + gap + padding. */\n private breakpoint = 0;\n\n constructor(private readonly el: ElementRef<HTMLElement>) {\n afterNextRender(() => {\n this.measureBreakpoint();\n this.checkLayout();\n });\n }\n\n ngOnChanges(): void {\n // Wait for Angular to update the DOM with the new links before re-measuring.\n requestAnimationFrame(() => {\n this.measureBreakpoint();\n this.checkLayout();\n });\n }\n\n @HostListener('window:resize')\n protected onResize(): void {\n this.checkLayout();\n }\n\n /**\n * Measures the natural widths of the copyright and links elements once and\n * stores their sum (plus padding and gap) as the layout breakpoint.\n */\n private measureBreakpoint(): void {\n const host = this.el.nativeElement;\n const copyrightEl = host.querySelector<HTMLElement>('.compsych-footer__copyright');\n const linksEl = host.querySelector<HTMLElement>('.compsych-footer__links');\n if (!copyrightEl) return;\n\n const style = getComputedStyle(host);\n const paddingInline = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);\n const gap = parseFloat(style.columnGap) || 0;\n\n this.breakpoint =\n copyrightEl.scrollWidth +\n (linksEl ? linksEl.scrollWidth + gap : 0) +\n paddingInline;\n }\n\n /** Switches to the row layout when the host is wide enough for both children. */\n private checkLayout(): void {\n if (!this.breakpoint) return;\n this.isRow.set(this.el.nativeElement.offsetWidth >= this.breakpoint);\n }\n}\n","@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"]}
|
package/lib/footer/footer.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
}
|