@ng-zen/cli 22.9.0-next.1 → 22.10.0-next.1

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.
Files changed (41) hide show
  1. package/package.json +3 -1
  2. package/schematics/ui/files/checkbox/checkbox.html +36 -0
  3. package/schematics/ui/files/checkbox/checkbox.scss +54 -31
  4. package/schematics/ui/files/checkbox/checkbox.spec.ts +94 -2
  5. package/schematics/ui/files/checkbox/checkbox.stories.ts +8 -28
  6. package/schematics/ui/files/checkbox/checkbox.ts +8 -21
  7. package/schematics/ui/files/form-control/form-control.spec.ts +18 -1
  8. package/schematics/ui/files/form-control/form-control.stories.ts +19 -4
  9. package/schematics/ui/files/form-control/form-control.ts +30 -4
  10. package/schematics/ui/files/hint/hint.scss +48 -0
  11. package/schematics/ui/files/hint/hint.spec.ts +54 -0
  12. package/schematics/ui/files/hint/hint.stories.ts +57 -0
  13. package/schematics/ui/files/hint/hint.ts +48 -0
  14. package/schematics/ui/files/hint/index.ts +1 -0
  15. package/schematics/ui/files/input/input.html +29 -0
  16. package/schematics/ui/files/input/input.scss +2 -1
  17. package/schematics/ui/files/input/input.spec.ts +107 -2
  18. package/schematics/ui/files/input/input.stories.ts +23 -16
  19. package/schematics/ui/files/input/input.ts +7 -14
  20. package/schematics/ui/files/label/index.ts +1 -0
  21. package/schematics/ui/files/label/label.html +6 -0
  22. package/schematics/ui/files/label/label.scss +52 -0
  23. package/schematics/ui/files/label/label.spec.ts +77 -0
  24. package/schematics/ui/files/label/label.stories.ts +67 -0
  25. package/schematics/ui/files/label/label.ts +57 -0
  26. package/schematics/ui/files/radio/radio.html +34 -0
  27. package/schematics/ui/files/radio/radio.scss +40 -27
  28. package/schematics/ui/files/radio/radio.spec.ts +40 -0
  29. package/schematics/ui/files/radio/radio.stories.ts +12 -25
  30. package/schematics/ui/files/radio/radio.ts +8 -20
  31. package/schematics/ui/files/switch/switch.html +31 -0
  32. package/schematics/ui/files/switch/switch.scss +34 -2
  33. package/schematics/ui/files/switch/switch.spec.ts +102 -2
  34. package/schematics/ui/files/switch/switch.stories.ts +12 -13
  35. package/schematics/ui/files/switch/switch.ts +7 -17
  36. package/schematics/ui/index.js +18 -8
  37. package/schematics/ui/index.js.map +1 -1
  38. package/schematics/ui/index.ts +19 -7
  39. package/schematics/ui/schema.js.map +1 -1
  40. package/schematics/ui/schema.json +2 -0
  41. package/schematics/ui/schema.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-zen/cli",
3
- "version": "22.9.0-next.1",
3
+ "version": "22.10.0-next.1",
4
4
  "description": "Angular CLI generator for creating fully customizable UI components.",
5
5
  "license": "BSD-2-Clause",
6
6
  "private": false,
@@ -61,8 +61,10 @@
61
61
  "divider",
62
62
  "drawer",
63
63
  "form-control",
64
+ "hint",
64
65
  "icon",
65
66
  "input",
67
+ "label",
66
68
  "pin",
67
69
  "popover",
68
70
  "radio",
@@ -0,0 +1,36 @@
1
+ <input
2
+ #inputElement
3
+ type="checkbox"
4
+ [attr.aria-disabled]="disabled()"
5
+ [attr.aria-invalid]="invalid() || null"
6
+ [checked]="value()"
7
+ [disabled]="disabled()"
8
+ [id]="id()"
9
+ (change)="onInput(inputElement.checked)"
10
+ />
11
+ <span aria-hidden="true" class="symbol">
12
+ @if (value() === null) {
13
+ ─
14
+ } @else if (value()) {
15
+ ✓
16
+ }
17
+ <!-- @else { ✕ } -->
18
+ </span>
19
+ @if (label()) {
20
+ <zen-label [disabled]="disabled()" [for]="id()" [required]="required()">{{ label() }}</zen-label>
21
+ }
22
+ @if (shouldShowError()) {
23
+ @for (error of errors(); track $index) {
24
+ <zen-hint severity="error">
25
+ @if (errorTemplate()) {
26
+ <ng-container *ngTemplateOutlet="errorTemplate()!; context: { $implicit: error }" />
27
+ } @else {
28
+ {{ error.message }}
29
+ }
30
+ </zen-hint>
31
+ }
32
+ } @else if (warn()) {
33
+ <zen-hint severity="warning">{{ warn() }}</zen-hint>
34
+ } @else if (hint()) {
35
+ <zen-hint>{{ hint() }}</zen-hint>
36
+ }
@@ -12,58 +12,81 @@ $disabled-opacity: var(--zen-disabled-opacity, 0.6);
12
12
  --zen-checkbox-border: 1px solid hsl(0deg 0% 80%);
13
13
  }
14
14
 
15
- input {
16
- position: absolute;
17
- cursor: pointer;
18
- opacity: 0;
19
- height: 100%;
20
- width: 100%;
21
- padding: 0;
22
- margin: 0;
23
- top: 0;
24
- left: 0;
25
- }
26
-
27
15
  /* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
28
16
  :host {
17
+ display: inline-grid;
18
+ grid-template-columns: var(--zen-checkbox-size) 1fr;
19
+ gap: 0.25rem 0.5rem;
20
+ align-items: center;
21
+
22
+ &:has(input[type='checkbox']:disabled) {
23
+ opacity: $disabled-opacity;
24
+
25
+ &,
26
+ input,
27
+ label {
28
+ cursor: not-allowed;
29
+ }
30
+ }
31
+ }
32
+
33
+ input[type='checkbox'] {
34
+ appearance: none;
35
+ grid-column: 1;
36
+ grid-row: 1;
29
37
  border: var(--zen-checkbox-border);
30
38
  border-radius: var(--zen-checkbox-border-radius);
31
39
  height: var(--zen-checkbox-size);
32
40
  width: var(--zen-checkbox-size);
33
41
  background-color: white;
34
42
  cursor: pointer;
35
- position: relative;
43
+ margin: 0;
44
+ padding: 0;
36
45
  transition:
37
46
  background-color ease,
38
47
  border-color ease;
39
48
  transition-duration: $transition-duration;
40
- user-select: none;
41
- justify-content: center;
42
- display: grid;
43
- place-items: center;
44
- font-size: small;
45
49
 
46
- &:has(input:checked) {
47
- color: white;
50
+ &:checked,
51
+ &:indeterminate {
48
52
  background-color: var(--zen-checkbox-appearance);
49
53
  border-color: var(--zen-checkbox-appearance);
50
54
  }
51
55
 
52
- &:has(input[type='checkbox']:disabled) {
53
- opacity: $disabled-opacity;
54
-
55
- &,
56
- input {
57
- cursor: not-allowed;
58
- }
59
- }
60
-
61
- &:has(input:focus-visible) {
56
+ &:focus-visible {
62
57
  outline: $outline;
63
58
  box-shadow: $focus-shadow;
64
59
  }
65
60
 
66
- &:has(input:user-invalid) {
61
+ &:user-invalid {
67
62
  border-color: $error;
68
63
  }
69
64
  }
65
+
66
+ zen-label {
67
+ grid-column: 2;
68
+ grid-row: 1;
69
+ }
70
+
71
+ zen-hint {
72
+ grid-column: 1 / -1;
73
+ }
74
+
75
+ .symbol {
76
+ grid-column: 1;
77
+ grid-row: 1;
78
+ place-self: center center;
79
+ pointer-events: none;
80
+ font-size: small;
81
+ line-height: 1;
82
+ color: white;
83
+ user-select: none;
84
+
85
+ &:empty {
86
+ display: none;
87
+ }
88
+ }
89
+
90
+ input[type='checkbox']:not(:checked, :indeterminate) ~ .symbol {
91
+ color: var(--zen-checkbox-appearance);
92
+ }
@@ -1,13 +1,38 @@
1
- import { provideZonelessChangeDetection } from '@angular/core';
1
+ import { Component, provideZonelessChangeDetection, signal } from '@angular/core';
2
2
  import { TestBed } from '@angular/core/testing';
3
+ import type { ValidationError } from '@angular/forms/signals';
3
4
  import { beforeEach, describe, expect, it } from 'vitest';
4
5
 
5
6
  import { ZenCheckbox } from './checkbox';
6
7
 
8
+ @Component({
9
+ imports: [ZenCheckbox],
10
+ template: `
11
+ <zen-checkbox
12
+ [errors]="errors()"
13
+ [hint]="hint()"
14
+ [invalid]="invalid()"
15
+ [label]="label()"
16
+ [required]="required()"
17
+ [touched]="touched()"
18
+ [warn]="warn()"
19
+ />
20
+ `,
21
+ })
22
+ class TestCheckboxHostComponent {
23
+ readonly label = signal('');
24
+ readonly hint = signal('');
25
+ readonly warn = signal('');
26
+ readonly invalid = signal(false);
27
+ readonly touched = signal(false);
28
+ readonly required = signal(false);
29
+ readonly errors = signal<ValidationError.WithOptionalFieldTree[]>([]);
30
+ }
31
+
7
32
  describe('ZenCheckbox', () => {
8
33
  beforeEach(async () => {
9
34
  await TestBed.configureTestingModule({
10
- imports: [ZenCheckbox],
35
+ imports: [ZenCheckbox, TestCheckboxHostComponent],
11
36
  providers: [provideZonelessChangeDetection()],
12
37
  }).compileComponents();
13
38
  });
@@ -17,4 +42,71 @@ describe('ZenCheckbox', () => {
17
42
  const component = fixture.componentInstance;
18
43
  expect(component).toBeTruthy();
19
44
  });
45
+
46
+ it('should render label linked to checkbox id and handle required state', () => {
47
+ const fixture = TestBed.createComponent(TestCheckboxHostComponent);
48
+ fixture.componentInstance.label.set('Accept terms');
49
+ fixture.detectChanges();
50
+
51
+ const labelEl = fixture.nativeElement.querySelector('label') as HTMLLabelElement;
52
+ const inputEl = fixture.nativeElement.querySelector('input') as HTMLInputElement;
53
+
54
+ expect(labelEl).toBeTruthy();
55
+ expect(labelEl.textContent?.trim()).toBe('Accept terms');
56
+ expect(labelEl.getAttribute('for')).toBe(inputEl.id);
57
+
58
+ fixture.componentInstance.required.set(true);
59
+ fixture.detectChanges();
60
+
61
+ const requiredSpan = fixture.nativeElement.querySelector('zen-label .required');
62
+ expect(requiredSpan).toBeTruthy();
63
+ expect(requiredSpan.textContent).toBe('*');
64
+ });
65
+
66
+ it('should handle indeterminate state and display indeterminate symbol', () => {
67
+ const fixture = TestBed.createComponent(ZenCheckbox);
68
+ fixture.componentRef.setInput('value', null);
69
+ fixture.detectChanges();
70
+
71
+ const inputEl = fixture.nativeElement.querySelector('input') as HTMLInputElement;
72
+ expect(inputEl.indeterminate).toBe(true);
73
+
74
+ const symbolEl = fixture.nativeElement.querySelector('.symbol');
75
+ expect(symbolEl?.textContent?.trim()).toBe('─');
76
+ });
77
+
78
+ it('should reset indeterminate state when value changes to boolean', () => {
79
+ const fixture = TestBed.createComponent(ZenCheckbox);
80
+ fixture.componentRef.setInput('value', null);
81
+ fixture.detectChanges();
82
+
83
+ const inputEl = fixture.nativeElement.querySelector('input') as HTMLInputElement;
84
+ expect(inputEl.indeterminate).toBe(true);
85
+
86
+ fixture.componentRef.setInput('value', true);
87
+ fixture.detectChanges();
88
+ expect(inputEl.indeterminate).toBe(false);
89
+ });
90
+
91
+ it('should display checkmark symbol when checked', () => {
92
+ const fixture = TestBed.createComponent(ZenCheckbox);
93
+ fixture.componentRef.setInput('value', true);
94
+ fixture.detectChanges();
95
+
96
+ const symbolEl = fixture.nativeElement.querySelector('.symbol');
97
+ expect(symbolEl?.textContent?.trim()).toBe('✓');
98
+ });
99
+
100
+ it('should render errors when invalid and touched', () => {
101
+ const fixture = TestBed.createComponent(TestCheckboxHostComponent);
102
+ fixture.componentInstance.invalid.set(true);
103
+ fixture.componentInstance.touched.set(true);
104
+ fixture.componentInstance.errors.set([{ kind: 'required', message: 'Agreement is required' }]);
105
+ fixture.detectChanges();
106
+
107
+ const hintEl = fixture.nativeElement.querySelector('zen-hint');
108
+ expect(hintEl).toBeTruthy();
109
+ expect(hintEl.getAttribute('data-severity')).toBe('error');
110
+ expect(hintEl.textContent?.trim()).toBe('Agreement is required');
111
+ });
20
112
  });
@@ -40,13 +40,12 @@ type Story = StoryObj<Options>;
40
40
  export const Default: Story = {};
41
41
 
42
42
  export const WithLabel: Story = {
43
- render: () => ({
44
- template: `
45
- <div style="display: flex; align-items: center; gap: 0.25rem">
46
- <zen-checkbox id="label-example"/>
47
- <label for="label-example"> With label </label>
48
- </div>
49
- `,
43
+ args: {
44
+ label: 'I agree to the terms and conditions',
45
+ },
46
+ render: args => ({
47
+ props: args,
48
+ template: '<zen-checkbox [label]="label" />',
50
49
  }),
51
50
  };
52
51
 
@@ -59,18 +58,7 @@ export const WithSignalForm: Story = {
59
58
  parameters: {
60
59
  docs: {
61
60
  source: {
62
- code: `
63
- <div style="display: flex; flex-direction: column; gap: 0.5rem;">
64
- <div style="display: flex; align-items: center; gap: 0.25rem">
65
- <zen-checkbox id="agree" [formField]="form.agree" />
66
- <label for="agree">I agree to the terms</label>
67
- </div>
68
- @if (form.agree().invalid()) {
69
- <p style="color: red; margin: 0; font-size: 0.875rem;">
70
- {{ form.agree().errors()![0].message }}
71
- </p>
72
- }
73
- </div>`,
61
+ code: `<zen-checkbox label="I agree to the terms" [formField]="form.agree" />`,
74
62
  },
75
63
  },
76
64
  },
@@ -80,15 +68,7 @@ export const WithSignalForm: Story = {
80
68
  standalone: true,
81
69
  template: `
82
70
  <div style="display: flex; flex-direction: column; gap: 0.5rem;">
83
- <div style="display: flex; align-items: center; gap: 0.25rem">
84
- <zen-checkbox id="agree" [formField]="form.agree" />
85
- <label for="agree">I agree to the terms</label>
86
- </div>
87
- @if (form.agree().invalid()) {
88
- <p style="color: red; margin: 0; font-size: 0.875rem;">
89
- {{ form.agree().errors()![0].message }}
90
- </p>
91
- }
71
+ <zen-checkbox label="I agree to the terms" [formField]="form.agree" />
92
72
  </div>
93
73
  `,
94
74
  imports: [FormField, ZenCheckbox],
@@ -1,6 +1,9 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import { Component, effect, ElementRef, model, viewChild } from '@angular/core';
2
3
 
3
4
  import { ZenFormControl } from '../form-control';
5
+ import { ZenHint } from '../hint';
6
+ import { ZenLabel } from '../label';
4
7
 
5
8
  /**
6
9
  * ZenCheckbox is a checkbox component supporting checked (`true`),
@@ -9,8 +12,7 @@ import { ZenFormControl } from '../form-control';
9
12
  * Connect it to a Signal Forms field with `[formField]`:
10
13
  *
11
14
  * ```html
12
- * <zen-checkbox [formField]="myForm.agree" />
13
- * <label for="agree">I agree to the terms</label>
15
+ * <zen-checkbox [formField]="myForm.agree" label="I agree to the terms" />
14
16
  * ```
15
17
  *
16
18
  * When `value` is `null` the native checkbox is set to indeterminate via
@@ -37,24 +39,9 @@ import { ZenFormControl } from '../form-control';
37
39
  */
38
40
  @Component({
39
41
  selector: 'zen-checkbox',
40
- template: `
41
- <input
42
- #inputElement
43
- type="checkbox"
44
- [attr.aria-disabled]="disabled()"
45
- [attr.aria-invalid]="invalid() || null"
46
- [checked]="value()"
47
- [disabled]="disabled()"
48
- (change)="onInput(inputElement.checked)"
49
- />
50
- @if (inputElement.indeterminate) {
51
- ─
52
- } @else if (inputElement.checked) {
53
- ✓
54
- }
55
- <!-- @else { ✕ } -->
56
- `,
57
- styleUrls: ['./checkbox.scss'],
42
+ imports: [NgTemplateOutlet, ZenHint, ZenLabel],
43
+ templateUrl: './checkbox.html',
44
+ styleUrl: './checkbox.scss',
58
45
  })
59
46
  export class ZenCheckbox extends ZenFormControl<boolean | null> {
60
47
  /**
@@ -68,7 +55,7 @@ export class ZenCheckbox extends ZenFormControl<boolean | null> {
68
55
  super();
69
56
 
70
57
  effect(() => {
71
- if (this.value() === null) this.inputElement().nativeElement.indeterminate = true;
58
+ this.inputElement().nativeElement.indeterminate = this.value() === null;
72
59
  });
73
60
  }
74
61
  }
@@ -38,6 +38,23 @@ describe('FormControl', () => {
38
38
  expect(component.readonly()).toBe(false);
39
39
  expect(component.name()).toBe('');
40
40
  expect(component.errors()).toEqual([]);
41
- expect(component.disabledReasons()).toEqual([]);
41
+ expect(component.label()).toBe('');
42
+ expect(component.hint()).toBe('');
43
+ expect(component.warn()).toBe('');
44
+ expect(component.id()).toMatch(/^zen-control-\d+$/);
45
+ });
46
+
47
+ it('should compute shouldShowError based on invalid, touched/dirty, and errors', () => {
48
+ const fixture = TestBed.createComponent(TestFormControl);
49
+ const component = fixture.componentInstance;
50
+ expect(component.shouldShowError()).toBe(false);
51
+
52
+ fixture.componentRef.setInput('invalid', true);
53
+ fixture.componentRef.setInput('touched', true);
54
+ fixture.componentRef.setInput('errors', [{ message: 'Required' }]);
55
+ expect(component.shouldShowError()).toBe(true);
56
+
57
+ fixture.componentRef.setInput('errors', []);
58
+ expect(component.shouldShowError()).toBe(false);
42
59
  });
43
60
  });
@@ -53,14 +53,26 @@ export default {
53
53
  control: 'text' as const,
54
54
  table: { category: 'models', type: { summary: 'string' } },
55
55
  },
56
+ label: {
57
+ control: 'text' as const,
58
+ table: { category: 'inputs', type: { summary: 'string' } },
59
+ },
60
+ hint: {
61
+ control: 'text' as const,
62
+ table: { category: 'inputs', type: { summary: 'string' } },
63
+ },
64
+ warn: {
65
+ control: 'text' as const,
66
+ table: { category: 'inputs', type: { summary: 'string' } },
67
+ },
68
+ id: {
69
+ control: 'text' as const,
70
+ table: { category: 'inputs', type: { summary: 'string' } },
71
+ },
56
72
  errors: {
57
73
  control: false as const,
58
74
  table: { category: 'models', type: { summary: 'ValidationError[]' }, readonly: true },
59
75
  },
60
- disabledReasons: {
61
- control: false as const,
62
- table: { category: 'models', type: { summary: 'DisabledReason[]' }, readonly: true },
63
- },
64
76
  value: {
65
77
  control: false as const,
66
78
  table: {
@@ -87,6 +99,9 @@ export default {
87
99
  readonly: false,
88
100
  name: '',
89
101
  value: '',
102
+ label: '',
103
+ hint: '',
104
+ warn: '',
90
105
  },
91
106
  parameters: {
92
107
  docs: {
@@ -1,5 +1,14 @@
1
- import { booleanAttribute, Directive, input, ModelSignal, output } from '@angular/core';
2
- import type { DisabledReason, ValidationError, WithOptionalFieldTree } from '@angular/forms/signals';
1
+ import {
2
+ booleanAttribute,
3
+ computed,
4
+ contentChild,
5
+ Directive,
6
+ input,
7
+ ModelSignal,
8
+ output,
9
+ TemplateRef,
10
+ } from '@angular/core';
11
+ import type { ValidationError } from '@angular/forms/signals';
3
12
  import { FormValueControl } from '@angular/forms/signals';
4
13
 
5
14
  /**
@@ -40,12 +49,29 @@ import { FormValueControl } from '@angular/forms/signals';
40
49
  },
41
50
  })
42
51
  export abstract class ZenFormControl<Value> implements FormValueControl<Value> {
52
+ private static uniqueId = 0;
53
+
54
+ /** HTML id attribute for the control and its label. Defaults to an auto-generated unique id. */
55
+ readonly id = input<string>(`zen-control-${ZenFormControl.uniqueId++}`);
56
+ /** Label text displayed for the form control. */
57
+ readonly label = input<string>('');
58
+ /** Informational hint message displayed when there are no errors. */
59
+ readonly hint = input<string>('');
60
+ /** Warning message displayed when there are no errors. */
61
+ readonly warn = input<string>('');
62
+
63
+ /** Custom template to render validation errors: `<ng-template #error let-error>` */
64
+ readonly errorTemplate = contentChild<TemplateRef<{ $implicit: ValidationError.WithOptionalFieldTree }>>('error');
65
+
66
+ /** Whether validation errors should be displayed (invalid + touched/dirty). */
67
+ readonly shouldShowError = computed(() => {
68
+ return this.invalid() && (this.touched() || this.dirty()) && this.errors().length > 0;
69
+ });
70
+
43
71
  /** Validation errors for the field. Auto-bound by the Signal Forms directive. */
44
72
  readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
45
73
  /** Whether the control is disabled. Auto-bound by the Signal Forms directive. */
46
74
  readonly disabled = input(false);
47
- /** Reasons why the field is disabled. Auto-bound by the Signal Forms directive. */
48
- readonly disabledReasons = input<readonly WithOptionalFieldTree<DisabledReason>[]>([]);
49
75
  /** Whether the field is read-only. Auto-bound by the Signal Forms directive. */
50
76
  readonly readonly = input<boolean>(false);
51
77
  /** Whether the field is hidden from view. Auto-bound by the Signal Forms directive. */
@@ -0,0 +1,48 @@
1
+ :host {
2
+ --zen-hint-neutral: var(--zen-neutral, hsl(30deg 5% 10%));
3
+ --zen-hint-primary: var(--zen-primary, hsl(193deg 58% 34%));
4
+ --zen-hint-success: var(--zen-success, hsl(142deg 38% 36%));
5
+ --zen-hint-warning: var(--zen-warning, hsl(36deg 72% 46%));
6
+ --zen-hint-danger: var(--zen-danger, hsl(348deg 52% 42%));
7
+ --zen-hint-info: var(--zen-info, hsl(214deg 34% 46%));
8
+ --zen-hint-font-size: 0.75rem;
9
+ }
10
+
11
+ /* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
12
+ :host {
13
+ display: block;
14
+ font-size: var(--zen-hint-font-size);
15
+ line-height: 1.4;
16
+ color: var(--zen-hint-neutral);
17
+ opacity: 0.8;
18
+
19
+ &[data-severity='info'] {
20
+ color: var(--zen-hint-info);
21
+ opacity: 1;
22
+ }
23
+
24
+ &[data-severity='warning'] {
25
+ color: var(--zen-hint-warning);
26
+ opacity: 1;
27
+ }
28
+
29
+ &[data-severity='error'],
30
+ &[data-severity='danger'] {
31
+ color: var(--zen-hint-danger);
32
+ opacity: 1;
33
+ }
34
+
35
+ &[data-severity='success'] {
36
+ color: var(--zen-hint-success);
37
+ opacity: 1;
38
+ }
39
+
40
+ &[data-severity='neutral'] {
41
+ color: var(--zen-hint-neutral);
42
+ }
43
+
44
+ &[data-severity='primary'] {
45
+ color: var(--zen-hint-primary);
46
+ opacity: 1;
47
+ }
48
+ }
@@ -0,0 +1,54 @@
1
+ import { provideZonelessChangeDetection } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+
5
+ import { ZenHint } from './hint';
6
+
7
+ describe('ZenHint', () => {
8
+ beforeEach(async () => {
9
+ await TestBed.configureTestingModule({
10
+ imports: [ZenHint],
11
+ providers: [provideZonelessChangeDetection()],
12
+ }).compileComponents();
13
+ });
14
+
15
+ it('should create', () => {
16
+ const fixture = TestBed.createComponent(ZenHint);
17
+ const component = fixture.componentInstance;
18
+ expect(component).toBeTruthy();
19
+ });
20
+
21
+ it('should set default severity to neutral and role to status', () => {
22
+ const fixture = TestBed.createComponent(ZenHint);
23
+ fixture.detectChanges();
24
+ const el = fixture.nativeElement as HTMLElement;
25
+ expect(el.getAttribute('data-severity')).toBe('neutral');
26
+ expect(el.getAttribute('role')).toBe('status');
27
+ });
28
+
29
+ it('should set role to alert when severity is error or danger', () => {
30
+ const fixture = TestBed.createComponent(ZenHint);
31
+ fixture.componentRef.setInput('severity', 'error');
32
+ fixture.detectChanges();
33
+ let el = fixture.nativeElement as HTMLElement;
34
+ expect(el.getAttribute('data-severity')).toBe('error');
35
+ expect(el.getAttribute('role')).toBe('alert');
36
+
37
+ fixture.componentRef.setInput('severity', 'danger');
38
+ fixture.detectChanges();
39
+ el = fixture.nativeElement as HTMLElement;
40
+ expect(el.getAttribute('data-severity')).toBe('danger');
41
+ expect(el.getAttribute('role')).toBe('alert');
42
+ });
43
+
44
+ it('should set role to status for info, warning, success, and primary', () => {
45
+ const fixture = TestBed.createComponent(ZenHint);
46
+ for (const severity of ['info', 'warning', 'success', 'primary'] as const) {
47
+ fixture.componentRef.setInput('severity', severity);
48
+ fixture.detectChanges();
49
+ const el = fixture.nativeElement as HTMLElement;
50
+ expect(el.getAttribute('data-severity')).toBe(severity);
51
+ expect(el.getAttribute('role')).toBe('status');
52
+ }
53
+ });
54
+ });
@@ -0,0 +1,57 @@
1
+ import { Meta, StoryObj } from '@storybook/angular';
2
+
3
+ import { ZenHint } from './hint';
4
+
5
+ type Options = ZenHint;
6
+
7
+ export default {
8
+ title: 'Ui/Hint',
9
+ component: ZenHint,
10
+ argTypes: {
11
+ severity: {
12
+ control: 'select',
13
+ options: ['info', 'warning', 'error', 'danger', 'success', 'neutral', 'primary'],
14
+ },
15
+ },
16
+ args: {
17
+ severity: 'neutral',
18
+ },
19
+ render: args => ({
20
+ props: args,
21
+ template: `<zen-hint [severity]="severity">This is a ${args.severity ?? 'neutral'} message.</zen-hint>`,
22
+ }),
23
+ } satisfies Meta<Options>;
24
+
25
+ type Story = StoryObj<Options>;
26
+
27
+ export const Default: Story = {};
28
+
29
+ export const ErrorSeverity: Story = {
30
+ args: {
31
+ severity: 'error',
32
+ },
33
+ render: args => ({
34
+ props: args,
35
+ template: '<zen-hint [severity]="severity">This field is required.</zen-hint>',
36
+ }),
37
+ };
38
+
39
+ export const WarningSeverity: Story = {
40
+ args: {
41
+ severity: 'warning',
42
+ },
43
+ render: args => ({
44
+ props: args,
45
+ template: '<zen-hint [severity]="severity">Password strength is moderate.</zen-hint>',
46
+ }),
47
+ };
48
+
49
+ export const SuccessSeverity: Story = {
50
+ args: {
51
+ severity: 'success',
52
+ },
53
+ render: args => ({
54
+ props: args,
55
+ template: '<zen-hint [severity]="severity">Username is available.</zen-hint>',
56
+ }),
57
+ };