@compsych-ui-components/angular 7.0.9 → 7.0.10

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.
@@ -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,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;;AAEjE,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;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAyBjE,MAAM,OAAO,qBAAqB;wGAArB,qBAAqB;yGAArB,qBAAqB,YApB9B,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,iBAAiB;YACjB,kBAAkB,aAGlB,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,iBAAiB;YACjB,kBAAkB;yGAGT,qBAAqB;;4FAArB,qBAAqB;kBAtBjC,QAAQ;mBAAC;oBACR,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,iBAAiB;wBACjB,kBAAkB;qBACnB;oBACD,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,iBAAiB;wBACjB,kBAAkB;qBACnB;iBACF","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\nimport { CardComponent } from './lib/card-base/card';\r\nimport { CardGridComponent } from './lib/card-grid/card-grid';\r\nimport { TextInputComponent } from './lib/text-input/text-input';\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\nexport { CardComponent } from './lib/card-base/card';\r\nexport type { CardVariant, CardAppearance, CardRadius, CardPadding, CardOrientation, CardHeadingLevel } from './lib/card-base/card';\r\nexport { RippleDirective } from './lib/card-base/ripple.directive';\r\nexport { CardGridComponent } from './lib/card-grid/card-grid';\r\nexport type { CardGridLayout } from './lib/card-grid/card-grid';\r\nexport { TextInputComponent } from './lib/text-input/text-input';\r\nexport type { TextInputSize, TextInputType } from './lib/text-input/text-input';\r\n\r\n@NgModule({\r\n imports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n CardGridComponent,\r\n TextInputComponent,\r\n ],\r\n exports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n CardGridComponent,\r\n TextInputComponent,\r\n ],\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;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;;AAEjE,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;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAyBjE,MAAM,OAAO,qBAAqB;wGAArB,qBAAqB;yGAArB,qBAAqB,YApB9B,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,iBAAiB;YACjB,kBAAkB,aAGlB,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,iBAAiB;YACjB,kBAAkB;yGAGT,qBAAqB;;4FAArB,qBAAqB;kBAtBjC,QAAQ;mBAAC;oBACR,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,iBAAiB;wBACjB,kBAAkB;qBACnB;oBACD,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,iBAAiB;wBACjB,kBAAkB;qBACnB;iBACF","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\nimport { CardComponent } from './lib/card-base/card';\r\nimport { CardGridComponent } from './lib/card-grid/card-grid';\r\nimport { TextInputComponent } from './lib/text-input/text-input';\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\nexport { CardComponent } from './lib/card-base/card';\r\nexport type { CardVariant, CardAppearance, CardRadius, CardPadding, CardOrientation, CardHeadingLevel } from './lib/card-base/card';\r\nexport { RippleDirective } from './lib/card-base/ripple.directive';\r\nexport { CardGridComponent } from './lib/card-grid/card-grid';\r\nexport type { CardGridLayout } from './lib/card-grid/card-grid';\r\nexport { TextInputComponent } from './lib/text-input/text-input';\r\nexport type { TextInputSize, TextInputType, TextInputLabelButton } from './lib/text-input/text-input';\r\n\r\n@NgModule({\r\n imports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n CardGridComponent,\r\n TextInputComponent,\r\n ],\r\n exports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n CardGridComponent,\r\n TextInputComponent,\r\n ],\r\n})\r\nexport class CompsychAngularModule {}\r\n"]}
@@ -1,5 +1,6 @@
1
1
  import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, booleanAttribute, inject, } from '@angular/core';
2
2
  import { NgControl, Validators } from '@angular/forms';
3
+ import { ButtonComponent } from '../button/button';
3
4
  import * as i0 from "@angular/core";
4
5
  let nextId = 0;
5
6
  /**
@@ -42,6 +43,43 @@ export class TextInputComponent {
42
43
  * Forwarded to `aria-describedby` on the native `<input>`.
43
44
  */
44
45
  ariaDescribedBy;
46
+ /** Text displayed below the input. Shown in red when the input is in error state. */
47
+ supportingText = '';
48
+ /**
49
+ * Static error text shown in red when the input is in error state.
50
+ * Used when the input is outside a form (with `[error]="true"`).
51
+ * When inside a form, prefer `errorMessages` for per-validator messages.
52
+ */
53
+ errorMessage = '';
54
+ /**
55
+ * Map of Angular validator error keys to display strings.
56
+ * The component reads `ngControl.errors` to determine which validator failed
57
+ * and shows the matching message in red below the input.
58
+ * Example: `{ required: 'Username is required', minlength: 'Min 8 characters' }`
59
+ */
60
+ errorMessages = {};
61
+ _labelButton = null;
62
+ /**
63
+ * JSON string configuring the optional button at the right of the label row.
64
+ * Hidden when empty or omitted. Example: `'{"id":"show","label":"Show"}'`
65
+ * Clicking emits the button's `id` via the `labelButtonClick` output.
66
+ */
67
+ set labelButton(json) {
68
+ if (!json) {
69
+ this._labelButton = null;
70
+ return;
71
+ }
72
+ try {
73
+ this._labelButton = JSON.parse(json);
74
+ }
75
+ catch {
76
+ this._labelButton = null;
77
+ }
78
+ }
79
+ /** @internal Template access to the parsed label button config. */
80
+ get parsedLabelButton() {
81
+ return this._labelButton;
82
+ }
45
83
  /**
46
84
  * Adds a blue asterisk after the label. When used in a form, this is set
47
85
  * automatically if the bound `FormControl` has `Validators.required`.
@@ -61,6 +99,8 @@ export class TextInputComponent {
61
99
  readonly = false;
62
100
  /** Emits the new string value whenever the user types. For use outside Angular forms; inside forms use the `FormControl` value stream. */
63
101
  valueChange = new EventEmitter();
102
+ /** Emits the label button's `id` when it is clicked. */
103
+ labelButtonClick = new EventEmitter();
64
104
  get inputId() {
65
105
  return this.id ?? this._autoId;
66
106
  }
@@ -72,6 +112,31 @@ export class TextInputComponent {
72
112
  get isRequired() {
73
113
  return this.required || !!(this.ngControl?.control?.hasValidator(Validators.required));
74
114
  }
115
+ get activeErrorMessage() {
116
+ const errors = this.ngControl?.errors;
117
+ if (!errors)
118
+ return '';
119
+ for (const key of Object.keys(errors)) {
120
+ if (this.errorMessages[key])
121
+ return this.errorMessages[key];
122
+ }
123
+ return '';
124
+ }
125
+ get displayedSupportingText() {
126
+ if (this.hasError) {
127
+ return this.activeErrorMessage || this.errorMessage || this.supportingText;
128
+ }
129
+ return this.supportingText;
130
+ }
131
+ get describedBy() {
132
+ const parts = [];
133
+ if (this.ariaDescribedBy)
134
+ parts.push(this.ariaDescribedBy);
135
+ if (this.supportingText || this.errorMessage || Object.keys(this.errorMessages).length) {
136
+ parts.push(this.inputId + '-supporting');
137
+ }
138
+ return parts.length ? parts.join(' ') : null;
139
+ }
75
140
  get wrapperClasses() {
76
141
  return ['cmp-text-field', this.size, this.disabled ? 'disabled' : '']
77
142
  .filter(Boolean)
@@ -104,15 +169,34 @@ export class TextInputComponent {
104
169
  this._onTouched();
105
170
  this.cdr.markForCheck();
106
171
  }
172
+ onLabelButtonClick() {
173
+ if (this._labelButton) {
174
+ this.labelButtonClick.emit(this._labelButton.id);
175
+ }
176
+ }
107
177
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
108
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TextInputComponent, isStandalone: true, selector: "compsych-text-input", inputs: { label: "label", placeholder: "placeholder", value: "value", type: "type", size: "size", name: "name", id: "id", ariaDescribedBy: "ariaDescribedBy", required: ["required", "required", booleanAttribute], error: ["error", "error", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], readonly: ["readonly", "readonly", booleanAttribute] }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
178
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TextInputComponent, isStandalone: true, selector: "compsych-text-input", inputs: { label: "label", placeholder: "placeholder", value: "value", type: "type", size: "size", name: "name", id: "id", ariaDescribedBy: "ariaDescribedBy", supportingText: "supportingText", errorMessage: "errorMessage", errorMessages: "errorMessages", labelButton: "labelButton", required: ["required", "required", booleanAttribute], error: ["error", "error", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], readonly: ["readonly", "readonly", booleanAttribute] }, outputs: { valueChange: "valueChange", labelButtonClick: "labelButtonClick" }, ngImport: i0, template: `
109
179
  <div [class]="wrapperClasses">
110
- <label class="label" [for]="inputId">
111
- {{ label }}
112
- @if (isRequired) {
113
- <span class="required-marker" aria-hidden="true"> *</span>
180
+ <div class="label-row">
181
+ <label class="label" [for]="inputId">
182
+ {{ label }}
183
+ @if (isRequired) {
184
+ <span class="required-marker" aria-hidden="true"> *</span>
185
+ }
186
+ </label>
187
+ @if (parsedLabelButton) {
188
+ <compsych-button
189
+ [label]="parsedLabelButton.label"
190
+ [variant]="parsedLabelButton.variant ?? 'text'"
191
+ size="small"
192
+ [leadingIcon]="parsedLabelButton.leadingIcon"
193
+ [trailingIcon]="parsedLabelButton.trailingIcon"
194
+ [iconOnly]="!!parsedLabelButton.iconOnly"
195
+ [disabled]="disabled"
196
+ (click)="onLabelButtonClick()"
197
+ />
114
198
  }
115
- </label>
199
+ </div>
116
200
  <input
117
201
  class="input"
118
202
  [id]="inputId"
@@ -125,23 +209,42 @@ export class TextInputComponent {
125
209
  [attr.required]="isRequired ? '' : null"
126
210
  [class.error]="hasError"
127
211
  [attr.aria-invalid]="hasError ? 'true' : null"
128
- [attr.aria-describedby]="ariaDescribedBy || null"
212
+ [attr.aria-describedby]="describedBy"
129
213
  (input)="onInput($event)"
130
214
  (blur)="onBlur()"
131
215
  />
216
+ @if (displayedSupportingText) {
217
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="inputId + '-supporting'">
218
+ {{ displayedSupportingText }}
219
+ </span>
220
+ }
132
221
  </div>
133
- `, isInline: true, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
222
+ `, isInline: true, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.supporting-text{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface-variant)}.supporting-text--error{color:var(--error-sys-error)}.cmp-text-field.disabled .supporting-text{opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "compsych-button", inputs: ["label", "variant", "size", "leadingIcon", "trailingIcon", "type", "ariaLabel", "iconOnly", "disabled", "loading", "fullWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
134
223
  }
135
224
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextInputComponent, decorators: [{
136
225
  type: Component,
137
- args: [{ selector: 'compsych-text-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
226
+ args: [{ selector: 'compsych-text-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ButtonComponent], template: `
138
227
  <div [class]="wrapperClasses">
139
- <label class="label" [for]="inputId">
140
- {{ label }}
141
- @if (isRequired) {
142
- <span class="required-marker" aria-hidden="true"> *</span>
228
+ <div class="label-row">
229
+ <label class="label" [for]="inputId">
230
+ {{ label }}
231
+ @if (isRequired) {
232
+ <span class="required-marker" aria-hidden="true"> *</span>
233
+ }
234
+ </label>
235
+ @if (parsedLabelButton) {
236
+ <compsych-button
237
+ [label]="parsedLabelButton.label"
238
+ [variant]="parsedLabelButton.variant ?? 'text'"
239
+ size="small"
240
+ [leadingIcon]="parsedLabelButton.leadingIcon"
241
+ [trailingIcon]="parsedLabelButton.trailingIcon"
242
+ [iconOnly]="!!parsedLabelButton.iconOnly"
243
+ [disabled]="disabled"
244
+ (click)="onLabelButtonClick()"
245
+ />
143
246
  }
144
- </label>
247
+ </div>
145
248
  <input
146
249
  class="input"
147
250
  [id]="inputId"
@@ -154,12 +257,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
154
257
  [attr.required]="isRequired ? '' : null"
155
258
  [class.error]="hasError"
156
259
  [attr.aria-invalid]="hasError ? 'true' : null"
157
- [attr.aria-describedby]="ariaDescribedBy || null"
260
+ [attr.aria-describedby]="describedBy"
158
261
  (input)="onInput($event)"
159
262
  (blur)="onBlur()"
160
263
  />
264
+ @if (displayedSupportingText) {
265
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="inputId + '-supporting'">
266
+ {{ displayedSupportingText }}
267
+ </span>
268
+ }
161
269
  </div>
162
- `, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}}\n"] }]
270
+ `, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.supporting-text{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface-variant)}.supporting-text--error{color:var(--error-sys-error)}.cmp-text-field.disabled .supporting-text{opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}}\n"] }]
163
271
  }], ctorParameters: () => [], propDecorators: { label: [{
164
272
  type: Input
165
273
  }], placeholder: [{
@@ -176,6 +284,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
176
284
  type: Input
177
285
  }], ariaDescribedBy: [{
178
286
  type: Input
287
+ }], supportingText: [{
288
+ type: Input
289
+ }], errorMessage: [{
290
+ type: Input
291
+ }], errorMessages: [{
292
+ type: Input
293
+ }], labelButton: [{
294
+ type: Input
179
295
  }], required: [{
180
296
  type: Input,
181
297
  args: [{ transform: booleanAttribute }]
@@ -190,5 +306,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
190
306
  args: [{ transform: booleanAttribute }]
191
307
  }], valueChange: [{
192
308
  type: Output
309
+ }], labelButtonClick: [{
310
+ type: Output
193
311
  }] } });
194
312
  //# sourceMappingURL=text-input.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"text-input.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/text-input/text-input.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n Input,\n Output,\n booleanAttribute,\n inject,\n} from '@angular/core';\nimport { ControlValueAccessor, NgControl, Validators } from '@angular/forms';\n\n/** Size variant of a `compsych-text-input`. Controls input height, padding, and font size. */\nexport type TextInputSize = 'small' | 'medium' | 'large';\n\n/** Native input type. */\nexport type TextInputType = 'text' | 'password';\n\nlet nextId = 0;\n\n/**\n * A labelled text input that wraps a native `<input>` element.\n * Implements `ControlValueAccessor` so it works with both reactive forms\n * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`).\n * The error visual state is driven automatically by the bound `FormControl`'s\n * validity once the field has been touched, and can also be set explicitly via\n * the `error` input for use outside of Angular forms.\n */\n@Component({\n selector: 'compsych-text-input',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div [class]=\"wrapperClasses\">\n <label class=\"label\" [for]=\"inputId\">\n {{ label }}\n @if (isRequired) {\n <span class=\"required-marker\" aria-hidden=\"true\"> *</span>\n }\n </label>\n <input\n class=\"input\"\n [id]=\"inputId\"\n [name]=\"name || inputId\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [value]=\"value\"\n [disabled]=\"disabled\"\n [readOnly]=\"readonly\"\n [attr.required]=\"isRequired ? '' : null\"\n [class.error]=\"hasError\"\n [attr.aria-invalid]=\"hasError ? 'true' : null\"\n [attr.aria-describedby]=\"ariaDescribedBy || null\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n `,\n styleUrl: './text-input.css',\n})\nexport class TextInputComponent implements ControlValueAccessor {\n private readonly _autoId = `cmp-text-input-${++nextId}`;\n private readonly cdr = inject(ChangeDetectorRef);\n private _onChange: (value: string) => void = () => {};\n private _onTouched: () => void = () => {};\n\n /** Injected when the component is used inside a reactive or template-driven form. */\n protected readonly ngControl: NgControl | null;\n\n constructor() {\n const ctrl = inject(NgControl, { optional: true, self: true });\n if (ctrl) ctrl.valueAccessor = this;\n this.ngControl = ctrl;\n }\n\n /** Label displayed above the input. */\n @Input() label = '';\n /** Placeholder text shown inside the input when empty. */\n @Input() placeholder = '';\n /** Current value of the input. When used in a form, the form controls the value. */\n @Input() value = '';\n /** Native input type. Defaults to `'text'`. Use `'password'` to mask the value. */\n @Input() type: TextInputType = 'text';\n /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */\n @Input() size: TextInputSize = 'medium';\n /** Native `name` attribute forwarded to the `<input>`. Defaults to the auto-generated id. */\n @Input() name?: string;\n /** Native `id` attribute for the `<input>`. Auto-generated if omitted. */\n @Input() id?: string;\n /**\n * Id(s) of external elements that describe the input, space-separated.\n * Forwarded to `aria-describedby` on the native `<input>`.\n */\n @Input() ariaDescribedBy?: string;\n\n /**\n * Adds a blue asterisk after the label. When used in a form, this is set\n * automatically if the bound `FormControl` has `Validators.required`.\n * Can also be set explicitly for non-form use. Supports boolean coercion.\n */\n @Input({ transform: booleanAttribute }) required = false;\n /**\n * Applies the error visual state (red border) explicitly. When used in a form,\n * the error state is driven automatically from the `FormControl`'s validity once\n * the field is touched — this input is only needed outside of forms.\n * Supports boolean coercion.\n */\n @Input({ transform: booleanAttribute }) error = false;\n /** Disables the input. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) disabled = false;\n /** Makes the input read-only — focusable and copyable but not editable. Value is still submitted with the form. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) readonly = false;\n\n /** Emits the new string value whenever the user types. For use outside Angular forms; inside forms use the `FormControl` value stream. */\n @Output() readonly valueChange = new EventEmitter<string>();\n\n get inputId(): string {\n return this.id ?? this._autoId;\n }\n\n /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */\n get hasError(): boolean {\n return this.error || !!(this.ngControl?.invalid && this.ngControl.touched);\n }\n\n /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */\n get isRequired(): boolean {\n return this.required || !!(this.ngControl?.control?.hasValidator(Validators.required));\n }\n\n get wrapperClasses(): string {\n return ['cmp-text-field', this.size, this.disabled ? 'disabled' : '']\n .filter(Boolean)\n .join(' ');\n }\n\n // ── ControlValueAccessor ────────────────────────────────────────────────────\n\n writeValue(value: string | null): void {\n this.value = value ?? '';\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this._onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this._onTouched = fn;\n }\n\n /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n\n // ── Event handlers ──────────────────────────────────────────────────────────\n\n protected onInput(event: Event): void {\n const value = (event.target as HTMLInputElement).value;\n this.value = value;\n this._onChange(value);\n this.valueChange.emit(value);\n }\n\n protected onBlur(): void {\n this._onTouched();\n this.cdr.markForCheck();\n }\n}\n"]}
1
+ {"version":3,"file":"text-input.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/text-input/text-input.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n Input,\n Output,\n booleanAttribute,\n inject,\n} from '@angular/core';\nimport { ControlValueAccessor, NgControl, Validators } from '@angular/forms';\nimport { ButtonComponent, ButtonVariant } from '../button/button';\n\n/** Size variant of a `compsych-text-input`. Controls input height, padding, and font size. */\nexport type TextInputSize = 'small' | 'medium' | 'large';\n\n/** Native input type. */\nexport type TextInputType = 'text' | 'password';\n\n/** Configuration for the optional button rendered at the right of the label row. */\nexport interface TextInputLabelButton {\n /** Stable identifier emitted by `labelButtonClick` when clicked. */\n id: string;\n /** Text displayed on the button. */\n label: string;\n /** Visual style of the button. Defaults to `'text'`. */\n variant?: ButtonVariant;\n /** Lucide icon name shown before the label. */\n leadingIcon?: string;\n /** Lucide icon name shown after the label. */\n trailingIcon?: string;\n /** Hides the label and renders a square icon-only button. Requires `leadingIcon`. */\n iconOnly?: boolean;\n}\n\nlet nextId = 0;\n\n/**\n * A labelled text input that wraps a native `<input>` element.\n * Implements `ControlValueAccessor` so it works with both reactive forms\n * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`).\n * The error visual state is driven automatically by the bound `FormControl`'s\n * validity once the field has been touched, and can also be set explicitly via\n * the `error` input for use outside of Angular forms.\n */\n@Component({\n selector: 'compsych-text-input',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [ButtonComponent],\n template: `\n <div [class]=\"wrapperClasses\">\n <div class=\"label-row\">\n <label class=\"label\" [for]=\"inputId\">\n {{ label }}\n @if (isRequired) {\n <span class=\"required-marker\" aria-hidden=\"true\"> *</span>\n }\n </label>\n @if (parsedLabelButton) {\n <compsych-button\n [label]=\"parsedLabelButton.label\"\n [variant]=\"parsedLabelButton.variant ?? 'text'\"\n size=\"small\"\n [leadingIcon]=\"parsedLabelButton.leadingIcon\"\n [trailingIcon]=\"parsedLabelButton.trailingIcon\"\n [iconOnly]=\"!!parsedLabelButton.iconOnly\"\n [disabled]=\"disabled\"\n (click)=\"onLabelButtonClick()\"\n />\n }\n </div>\n <input\n class=\"input\"\n [id]=\"inputId\"\n [name]=\"name || inputId\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [value]=\"value\"\n [disabled]=\"disabled\"\n [readOnly]=\"readonly\"\n [attr.required]=\"isRequired ? '' : null\"\n [class.error]=\"hasError\"\n [attr.aria-invalid]=\"hasError ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n />\n @if (displayedSupportingText) {\n <span class=\"supporting-text\" [class.supporting-text--error]=\"hasError\" [id]=\"inputId + '-supporting'\">\n {{ displayedSupportingText }}\n </span>\n }\n </div>\n `,\n styleUrl: './text-input.css',\n})\nexport class TextInputComponent implements ControlValueAccessor {\n private readonly _autoId = `cmp-text-input-${++nextId}`;\n private readonly cdr = inject(ChangeDetectorRef);\n private _onChange: (value: string) => void = () => {};\n private _onTouched: () => void = () => {};\n\n /** Injected when the component is used inside a reactive or template-driven form. */\n protected readonly ngControl: NgControl | null;\n\n constructor() {\n const ctrl = inject(NgControl, { optional: true, self: true });\n if (ctrl) ctrl.valueAccessor = this;\n this.ngControl = ctrl;\n }\n\n /** Label displayed above the input. */\n @Input() label = '';\n /** Placeholder text shown inside the input when empty. */\n @Input() placeholder = '';\n /** Current value of the input. When used in a form, the form controls the value. */\n @Input() value = '';\n /** Native input type. Defaults to `'text'`. Use `'password'` to mask the value. */\n @Input() type: TextInputType = 'text';\n /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */\n @Input() size: TextInputSize = 'medium';\n /** Native `name` attribute forwarded to the `<input>`. Defaults to the auto-generated id. */\n @Input() name?: string;\n /** Native `id` attribute for the `<input>`. Auto-generated if omitted. */\n @Input() id?: string;\n /**\n * Id(s) of external elements that describe the input, space-separated.\n * Forwarded to `aria-describedby` on the native `<input>`.\n */\n @Input() ariaDescribedBy?: string;\n /** Text displayed below the input. Shown in red when the input is in error state. */\n @Input() supportingText = '';\n /**\n * Static error text shown in red when the input is in error state.\n * Used when the input is outside a form (with `[error]=\"true\"`).\n * When inside a form, prefer `errorMessages` for per-validator messages.\n */\n @Input() errorMessage = '';\n /**\n * Map of Angular validator error keys to display strings.\n * The component reads `ngControl.errors` to determine which validator failed\n * and shows the matching message in red below the input.\n * Example: `{ required: 'Username is required', minlength: 'Min 8 characters' }`\n */\n @Input() errorMessages: Record<string, string> = {};\n\n private _labelButton: TextInputLabelButton | null = null;\n\n /**\n * JSON string configuring the optional button at the right of the label row.\n * Hidden when empty or omitted. Example: `'{\"id\":\"show\",\"label\":\"Show\"}'`\n * Clicking emits the button's `id` via the `labelButtonClick` output.\n */\n @Input() set labelButton(json: string) {\n if (!json) { this._labelButton = null; return; }\n try {\n this._labelButton = JSON.parse(json) as TextInputLabelButton;\n } catch {\n this._labelButton = null;\n }\n }\n\n /** @internal Template access to the parsed label button config. */\n protected get parsedLabelButton(): TextInputLabelButton | null {\n return this._labelButton;\n }\n\n /**\n * Adds a blue asterisk after the label. When used in a form, this is set\n * automatically if the bound `FormControl` has `Validators.required`.\n * Can also be set explicitly for non-form use. Supports boolean coercion.\n */\n @Input({ transform: booleanAttribute }) required = false;\n /**\n * Applies the error visual state (red border) explicitly. When used in a form,\n * the error state is driven automatically from the `FormControl`'s validity once\n * the field is touched — this input is only needed outside of forms.\n * Supports boolean coercion.\n */\n @Input({ transform: booleanAttribute }) error = false;\n /** Disables the input. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) disabled = false;\n /** Makes the input read-only — focusable and copyable but not editable. Value is still submitted with the form. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) readonly = false;\n\n /** Emits the new string value whenever the user types. For use outside Angular forms; inside forms use the `FormControl` value stream. */\n @Output() readonly valueChange = new EventEmitter<string>();\n /** Emits the label button's `id` when it is clicked. */\n @Output() readonly labelButtonClick = new EventEmitter<string>();\n\n get inputId(): string {\n return this.id ?? this._autoId;\n }\n\n /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */\n get hasError(): boolean {\n return this.error || !!(this.ngControl?.invalid && this.ngControl.touched);\n }\n\n /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */\n get isRequired(): boolean {\n return this.required || !!(this.ngControl?.control?.hasValidator(Validators.required));\n }\n\n private get activeErrorMessage(): string {\n const errors = this.ngControl?.errors;\n if (!errors) return '';\n for (const key of Object.keys(errors)) {\n if (this.errorMessages[key]) return this.errorMessages[key];\n }\n return '';\n }\n\n get displayedSupportingText(): string {\n if (this.hasError) {\n return this.activeErrorMessage || this.errorMessage || this.supportingText;\n }\n return this.supportingText;\n }\n\n get describedBy(): string | null {\n const parts: string[] = [];\n if (this.ariaDescribedBy) parts.push(this.ariaDescribedBy);\n if (this.supportingText || this.errorMessage || Object.keys(this.errorMessages).length) {\n parts.push(this.inputId + '-supporting');\n }\n return parts.length ? parts.join(' ') : null;\n }\n\n get wrapperClasses(): string {\n return ['cmp-text-field', this.size, this.disabled ? 'disabled' : '']\n .filter(Boolean)\n .join(' ');\n }\n\n // ── ControlValueAccessor ────────────────────────────────────────────────────\n\n writeValue(value: string | null): void {\n this.value = value ?? '';\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this._onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this._onTouched = fn;\n }\n\n /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n\n // ── Event handlers ──────────────────────────────────────────────────────────\n\n protected onInput(event: Event): void {\n const value = (event.target as HTMLInputElement).value;\n this.value = value;\n this._onChange(value);\n this.valueChange.emit(value);\n }\n\n protected onBlur(): void {\n this._onTouched();\n this.cdr.markForCheck();\n }\n\n protected onLabelButtonClick(): void {\n if (this._labelButton) {\n this.labelButtonClick.emit(this._labelButton.id);\n }\n }\n}\n"]}
package/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export { RippleDirective } from './lib/card-base/ripple.directive';
23
23
  export { CardGridComponent } from './lib/card-grid/card-grid';
24
24
  export type { CardGridLayout } from './lib/card-grid/card-grid';
25
25
  export { TextInputComponent } from './lib/text-input/text-input';
26
- export type { TextInputSize, TextInputType } from './lib/text-input/text-input';
26
+ export type { TextInputSize, TextInputType, TextInputLabelButton } from './lib/text-input/text-input';
27
27
  export declare class CompsychAngularModule {
28
28
  static ɵfac: i0.ɵɵFactoryDeclaration<CompsychAngularModule, never>;
29
29
  static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.CardGridComponent, typeof i8.TextInputComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.CardGridComponent, typeof i8.TextInputComponent]>;
@@ -1,10 +1,26 @@
1
1
  import { EventEmitter } from '@angular/core';
2
2
  import { ControlValueAccessor, NgControl } from '@angular/forms';
3
+ import { ButtonVariant } from '../button/button';
3
4
  import * as i0 from "@angular/core";
4
5
  /** Size variant of a `compsych-text-input`. Controls input height, padding, and font size. */
5
6
  export type TextInputSize = 'small' | 'medium' | 'large';
6
7
  /** Native input type. */
7
8
  export type TextInputType = 'text' | 'password';
9
+ /** Configuration for the optional button rendered at the right of the label row. */
10
+ export interface TextInputLabelButton {
11
+ /** Stable identifier emitted by `labelButtonClick` when clicked. */
12
+ id: string;
13
+ /** Text displayed on the button. */
14
+ label: string;
15
+ /** Visual style of the button. Defaults to `'text'`. */
16
+ variant?: ButtonVariant;
17
+ /** Lucide icon name shown before the label. */
18
+ leadingIcon?: string;
19
+ /** Lucide icon name shown after the label. */
20
+ trailingIcon?: string;
21
+ /** Hides the label and renders a square icon-only button. Requires `leadingIcon`. */
22
+ iconOnly?: boolean;
23
+ }
8
24
  /**
9
25
  * A labelled text input that wraps a native `<input>` element.
10
26
  * Implements `ControlValueAccessor` so it works with both reactive forms
@@ -40,6 +56,30 @@ export declare class TextInputComponent implements ControlValueAccessor {
40
56
  * Forwarded to `aria-describedby` on the native `<input>`.
41
57
  */
42
58
  ariaDescribedBy?: string;
59
+ /** Text displayed below the input. Shown in red when the input is in error state. */
60
+ supportingText: string;
61
+ /**
62
+ * Static error text shown in red when the input is in error state.
63
+ * Used when the input is outside a form (with `[error]="true"`).
64
+ * When inside a form, prefer `errorMessages` for per-validator messages.
65
+ */
66
+ errorMessage: string;
67
+ /**
68
+ * Map of Angular validator error keys to display strings.
69
+ * The component reads `ngControl.errors` to determine which validator failed
70
+ * and shows the matching message in red below the input.
71
+ * Example: `{ required: 'Username is required', minlength: 'Min 8 characters' }`
72
+ */
73
+ errorMessages: Record<string, string>;
74
+ private _labelButton;
75
+ /**
76
+ * JSON string configuring the optional button at the right of the label row.
77
+ * Hidden when empty or omitted. Example: `'{"id":"show","label":"Show"}'`
78
+ * Clicking emits the button's `id` via the `labelButtonClick` output.
79
+ */
80
+ set labelButton(json: string);
81
+ /** @internal Template access to the parsed label button config. */
82
+ protected get parsedLabelButton(): TextInputLabelButton | null;
43
83
  /**
44
84
  * Adds a blue asterisk after the label. When used in a form, this is set
45
85
  * automatically if the bound `FormControl` has `Validators.required`.
@@ -59,11 +99,16 @@ export declare class TextInputComponent implements ControlValueAccessor {
59
99
  readonly: boolean;
60
100
  /** Emits the new string value whenever the user types. For use outside Angular forms; inside forms use the `FormControl` value stream. */
61
101
  readonly valueChange: EventEmitter<string>;
102
+ /** Emits the label button's `id` when it is clicked. */
103
+ readonly labelButtonClick: EventEmitter<string>;
62
104
  get inputId(): string;
63
105
  /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */
64
106
  get hasError(): boolean;
65
107
  /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */
66
108
  get isRequired(): boolean;
109
+ private get activeErrorMessage();
110
+ get displayedSupportingText(): string;
111
+ get describedBy(): string | null;
67
112
  get wrapperClasses(): string;
68
113
  writeValue(value: string | null): void;
69
114
  registerOnChange(fn: (value: string) => void): void;
@@ -72,8 +117,9 @@ export declare class TextInputComponent implements ControlValueAccessor {
72
117
  setDisabledState(isDisabled: boolean): void;
73
118
  protected onInput(event: Event): void;
74
119
  protected onBlur(): void;
120
+ protected onLabelButtonClick(): void;
75
121
  static ɵfac: i0.ɵɵFactoryDeclaration<TextInputComponent, never>;
76
- static ɵcmp: i0.ɵɵComponentDeclaration<TextInputComponent, "compsych-text-input", never, { "label": { "alias": "label"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "value": { "alias": "value"; "required": false; }; "type": { "alias": "type"; "required": false; }; "size": { "alias": "size"; "required": false; }; "name": { "alias": "name"; "required": false; }; "id": { "alias": "id"; "required": false; }; "ariaDescribedBy": { "alias": "ariaDescribedBy"; "required": false; }; "required": { "alias": "required"; "required": false; }; "error": { "alias": "error"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
122
+ static ɵcmp: i0.ɵɵComponentDeclaration<TextInputComponent, "compsych-text-input", never, { "label": { "alias": "label"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "value": { "alias": "value"; "required": false; }; "type": { "alias": "type"; "required": false; }; "size": { "alias": "size"; "required": false; }; "name": { "alias": "name"; "required": false; }; "id": { "alias": "id"; "required": false; }; "ariaDescribedBy": { "alias": "ariaDescribedBy"; "required": false; }; "supportingText": { "alias": "supportingText"; "required": false; }; "errorMessage": { "alias": "errorMessage"; "required": false; }; "errorMessages": { "alias": "errorMessages"; "required": false; }; "labelButton": { "alias": "labelButton"; "required": false; }; "required": { "alias": "required"; "required": false; }; "error": { "alias": "error"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; }, { "valueChange": "valueChange"; "labelButtonClick": "labelButtonClick"; }, never, never, true, never>;
77
123
  static ngAcceptInputType_required: unknown;
78
124
  static ngAcceptInputType_error: unknown;
79
125
  static ngAcceptInputType_disabled: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compsych-ui-components/angular",
3
- "version": "7.0.9",
3
+ "version": "7.0.10",
4
4
  "description": "Angular UI components for compsych",
5
5
  "publishConfig": {
6
6
  "access": "public",