@ng-zen/cli 22.9.0 → 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
@@ -0,0 +1,48 @@
1
+ import { Component, input } from '@angular/core';
2
+
3
+ export type ZenHintSeverity = 'info' | 'warning' | 'error' | 'danger' | 'success' | 'neutral' | 'primary';
4
+
5
+ /**
6
+ * ZenHint is a reusable component for displaying hints, helper text, warnings,
7
+ * and error messages for form controls.
8
+ *
9
+ * @example
10
+ * ```html
11
+ * <zen-hint>This is an info hint</zen-hint>
12
+ * <zen-hint severity="error">This field is required</zen-hint>
13
+ * <zen-hint severity="warning">Password strength is low</zen-hint>
14
+ * ```
15
+ *
16
+ * ### CSS Custom Properties
17
+ *
18
+ * You can customize the component using CSS custom properties:
19
+ *
20
+ * ```css
21
+ * :root {
22
+ * --zen-hint-neutral: var(--zen-neutral, hsl(30deg 5% 10%));
23
+ * --zen-hint-primary: var(--zen-primary, hsl(193deg 58% 34%));
24
+ * --zen-hint-success: var(--zen-success, hsl(142deg 38% 36%));
25
+ * --zen-hint-warning: var(--zen-warning, hsl(36deg 72% 46%));
26
+ * --zen-hint-danger: var(--zen-danger, hsl(348deg 52% 42%));
27
+ * --zen-hint-info: var(--zen-info, hsl(214deg 34% 46%));
28
+ * --zen-hint-font-size: 0.8125rem;
29
+ * }
30
+ * ```
31
+ *
32
+ * @author Konrad Stępień
33
+ * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
34
+ * @see [GitHub](https://github.com/kstepien3/ng-zen)
35
+ */
36
+ @Component({
37
+ selector: 'zen-hint',
38
+ template: '<ng-content />',
39
+ styleUrl: './hint.scss',
40
+ host: {
41
+ '[attr.data-severity]': 'severity()',
42
+ '[attr.role]': 'severity() === "error" || severity() === "danger" ? "alert" : "status"',
43
+ },
44
+ })
45
+ export class ZenHint {
46
+ /** The severity level of the hint message. */
47
+ readonly severity = input<ZenHintSeverity>('neutral');
48
+ }
@@ -0,0 +1 @@
1
+ export * from './hint';
@@ -0,0 +1,29 @@
1
+ @if (label()) {
2
+ <zen-label [disabled]="disabled()" [for]="id()" [required]="required()">{{ label() }}</zen-label>
3
+ }
4
+ <input
5
+ #inputRef
6
+ [aria-invalid]="invalid() || null"
7
+ [disabled]="disabled()"
8
+ [id]="id()"
9
+ [placeholder]="placeholder()"
10
+ [required]="required()"
11
+ [type]="type()"
12
+ [value]="value()"
13
+ (input)="onInput(inputRef.value)"
14
+ />
15
+ @if (shouldShowError()) {
16
+ @for (error of errors(); track $index) {
17
+ <zen-hint severity="error">
18
+ @if (errorTemplate()) {
19
+ <ng-container *ngTemplateOutlet="errorTemplate()!; context: { $implicit: error }" />
20
+ } @else {
21
+ {{ error.message }}
22
+ }
23
+ </zen-hint>
24
+ }
25
+ } @else if (warn()) {
26
+ <zen-hint severity="warning">{{ warn() }}</zen-hint>
27
+ } @else if (hint()) {
28
+ <zen-hint>{{ hint() }}</zen-hint>
29
+ }
@@ -13,7 +13,8 @@ $disabled-opacity: var(--zen-disabled-opacity, 0.6);
13
13
 
14
14
  /* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
15
15
  :host {
16
- display: contents;
16
+ display: inline-flex;
17
+ flex-direction: column;
17
18
  }
18
19
 
19
20
  input {
@@ -1,13 +1,43 @@
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 { ZenInput } from './input';
6
7
 
8
+ @Component({
9
+ imports: [ZenInput],
10
+ template: `
11
+ <zen-input
12
+ [errors]="errors()"
13
+ [hint]="hint()"
14
+ [invalid]="invalid()"
15
+ [label]="label()"
16
+ [required]="required()"
17
+ [touched]="touched()"
18
+ [warn]="warn()"
19
+ >
20
+ @if (customError()) {
21
+ <ng-template #error let-error>Custom: {{ error.message }}</ng-template>
22
+ }
23
+ </zen-input>
24
+ `,
25
+ })
26
+ class TestInputHostComponent {
27
+ readonly label = signal('');
28
+ readonly hint = signal('');
29
+ readonly warn = signal('');
30
+ readonly invalid = signal(false);
31
+ readonly touched = signal(false);
32
+ readonly required = signal(false);
33
+ readonly errors = signal<ValidationError.WithOptionalFieldTree[]>([]);
34
+ readonly customError = signal(false);
35
+ }
36
+
7
37
  describe('ZenInput', () => {
8
38
  beforeEach(async () => {
9
39
  await TestBed.configureTestingModule({
10
- imports: [ZenInput],
40
+ imports: [ZenInput, TestInputHostComponent],
11
41
  providers: [provideZonelessChangeDetection()],
12
42
  }).compileComponents();
13
43
  });
@@ -17,4 +47,79 @@ describe('ZenInput', () => {
17
47
  const component = fixture.componentInstance;
18
48
  expect(component).toBeTruthy();
19
49
  });
50
+
51
+ it('should render label linked to input id and handle required state', () => {
52
+ const fixture = TestBed.createComponent(TestInputHostComponent);
53
+ fixture.componentInstance.label.set('User Email');
54
+ fixture.detectChanges();
55
+
56
+ const labelEl = fixture.nativeElement.querySelector('label') as HTMLLabelElement;
57
+ const inputEl = fixture.nativeElement.querySelector('input') as HTMLInputElement;
58
+
59
+ expect(labelEl).toBeTruthy();
60
+ expect(labelEl.textContent?.trim()).toBe('User Email');
61
+ expect(labelEl.getAttribute('for')).toBe(inputEl.id);
62
+ expect(inputEl.id).toMatch(/^zen-control-\d+$/);
63
+
64
+ fixture.componentInstance.required.set(true);
65
+ fixture.detectChanges();
66
+
67
+ const requiredSpan = fixture.nativeElement.querySelector('zen-label .required');
68
+ expect(requiredSpan).toBeTruthy();
69
+ expect(requiredSpan.textContent).toBe('*');
70
+ });
71
+
72
+ it('should render hint when provided', () => {
73
+ const fixture = TestBed.createComponent(TestInputHostComponent);
74
+ fixture.componentInstance.hint.set('Informational hint');
75
+ fixture.detectChanges();
76
+
77
+ const hintEl = fixture.nativeElement.querySelector('zen-hint');
78
+ expect(hintEl).toBeTruthy();
79
+ expect(hintEl.getAttribute('data-severity')).toBe('neutral');
80
+ expect(hintEl.textContent?.trim()).toBe('Informational hint');
81
+ });
82
+
83
+ it('should render warn when provided and take precedence over hint', () => {
84
+ const fixture = TestBed.createComponent(TestInputHostComponent);
85
+ fixture.componentInstance.hint.set('Informational hint');
86
+ fixture.componentInstance.warn.set('Warning message');
87
+ fixture.detectChanges();
88
+
89
+ const hintEl = fixture.nativeElement.querySelector('zen-hint');
90
+ expect(hintEl).toBeTruthy();
91
+ expect(hintEl.getAttribute('data-severity')).toBe('warning');
92
+ expect(hintEl.textContent?.trim()).toBe('Warning message');
93
+ });
94
+
95
+ it('should render error messages when invalid and touched', () => {
96
+ const fixture = TestBed.createComponent(TestInputHostComponent);
97
+ fixture.componentInstance.invalid.set(true);
98
+ fixture.componentInstance.touched.set(true);
99
+ fixture.componentInstance.errors.set([
100
+ { kind: 'required', message: 'Field required' },
101
+ { kind: 'minLength', message: 'Min length 5' },
102
+ ]);
103
+ fixture.detectChanges();
104
+
105
+ const hintEls = fixture.nativeElement.querySelectorAll('zen-hint');
106
+ expect(hintEls.length).toBe(2);
107
+ expect(hintEls[0].getAttribute('data-severity')).toBe('error');
108
+ expect(hintEls[0].textContent?.trim()).toBe('Field required');
109
+ expect(hintEls[1].textContent?.trim()).toBe('Min length 5');
110
+ });
111
+
112
+ it('should allow overriding error message via ng-template #error', () => {
113
+ const fixture = TestBed.createComponent(TestInputHostComponent);
114
+ fixture.componentInstance.customError.set(true);
115
+ fixture.componentInstance.invalid.set(true);
116
+ fixture.componentInstance.touched.set(true);
117
+ fixture.componentInstance.errors.set([{ kind: 'required', message: 'Field required' }]);
118
+ fixture.detectChanges();
119
+
120
+ const hintEl = fixture.nativeElement.querySelector('zen-hint');
121
+ expect(hintEl).toBeTruthy();
122
+ expect(hintEl.getAttribute('data-severity')).toBe('error');
123
+ expect(hintEl.textContent?.trim()).toBe('Custom: Field required');
124
+ });
20
125
  });
@@ -41,13 +41,25 @@ type Story = StoryObj<Options>;
41
41
  export const Default: Story = {};
42
42
 
43
43
  export const WithLabel: Story = {
44
- render: () => ({
45
- template: `
46
- <div style="display: flex; flex-direction: column">
47
- <label for="label-example"> With label </label>
48
- <zen-input id="label-example"/>
49
- </div>
50
- `,
44
+ args: {
45
+ label: 'Username',
46
+ placeholder: 'Enter username',
47
+ },
48
+ render: args => ({
49
+ props: args,
50
+ template: '<zen-input [label]="label" [placeholder]="placeholder" />',
51
+ }),
52
+ };
53
+
54
+ export const WithHintAndWarn: Story = {
55
+ args: {
56
+ label: 'Password',
57
+ hint: 'Must be at least 8 characters',
58
+ placeholder: 'Enter password',
59
+ },
60
+ render: args => ({
61
+ props: args,
62
+ template: '<zen-input [label]="label" [hint]="hint" [placeholder]="placeholder" type="password" />',
51
63
  }),
52
64
  };
53
65
 
@@ -60,7 +72,7 @@ export const WithSignalForm: Story = {
60
72
  parameters: {
61
73
  docs: {
62
74
  source: {
63
- code: `<zen-input [formField]="form.name" />`,
75
+ code: `<zen-input label="Full Name" [formField]="form.name" />`,
64
76
  },
65
77
  },
66
78
  },
@@ -76,8 +88,8 @@ export const WithFormRoot: Story = {
76
88
  docs: {
77
89
  source: {
78
90
  code: `<form [formRoot]="loginForm">
79
- <zen-input [formField]="loginForm.email" placeholder="Email" />
80
- <zen-input [formField]="loginForm.password" placeholder="Password" type="password" />
91
+ <zen-input label="Email" [formField]="loginForm.email" placeholder="Email" />
92
+ <zen-input label="Password" [formField]="loginForm.password" placeholder="Password" type="password" />
81
93
  <button type="submit">Sign in</button>
82
94
  </form>`,
83
95
  },
@@ -89,12 +101,7 @@ export const WithFormRoot: Story = {
89
101
  standalone: true,
90
102
  template: `
91
103
  <div style="display: flex; flex-direction: column; gap: 0.5rem; max-width: 300px;">
92
- <zen-input placeholder="Type something" [formField]="form.name" />
93
- @if (form.name().invalid()) {
94
- <p style="color: red; margin: 0; font-size: 0.875rem;">
95
- {{ form.name().errors()![0].message }}
96
- </p>
97
- }
104
+ <zen-input label="Full Name" placeholder="Type something" [formField]="form.name" />
98
105
  </div>
99
106
  `,
100
107
  imports: [FormField, ZenInput],
@@ -1,6 +1,9 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import { Component, input, model } 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
  * ZenInput is a single-line text input component backed by Signal Forms.
@@ -8,7 +11,7 @@ import { ZenFormControl } from '../form-control';
8
11
  * Connect it to a Signal Forms field with `[formField]`:
9
12
  *
10
13
  * ```html
11
- * <zen-input [formField]="myForm.name" placeholder="Enter name" />
14
+ * <zen-input [formField]="myForm.name" placeholder="Enter name" label="Name" />
12
15
  * ```
13
16
  *
14
17
  * The `[formField]` directive automatically synchronises value, disabled
@@ -35,19 +38,9 @@ import { ZenFormControl } from '../form-control';
35
38
  */
36
39
  @Component({
37
40
  selector: 'zen-input',
38
- template: `
39
- <input
40
- #inputRef
41
- [aria-invalid]="invalid() || null"
42
- [disabled]="disabled()"
43
- [placeholder]="placeholder()"
44
- [required]="required()"
45
- [type]="type()"
46
- [value]="value()"
47
- (input)="onInput(inputRef.value)"
48
- />
49
- `,
50
- styleUrls: ['./input.scss'],
41
+ imports: [NgTemplateOutlet, ZenHint, ZenLabel],
42
+ templateUrl: './input.html',
43
+ styleUrl: './input.scss',
51
44
  })
52
45
  export class ZenInput extends ZenFormControl<string> {
53
46
  /** The current input value with two-way binding support. */
@@ -0,0 +1 @@
1
+ export * from './label';
@@ -0,0 +1,6 @@
1
+ <label [attr.for]="targetId()">
2
+ <ng-content />
3
+ @if (required()) {
4
+ <span aria-hidden="true" class="required">*</span>
5
+ }
6
+ </label>
@@ -0,0 +1,52 @@
1
+ $disabled-opacity: var(--zen-disabled-opacity, 0.6);
2
+
3
+ :host {
4
+ --zen-label-font-size: 0.875rem;
5
+ --zen-label-font-weight: 500;
6
+ --zen-label-color: hsl(0deg 0% 15%);
7
+ --zen-label-required-color: var(--zen-danger, hsl(348deg 52% 42%));
8
+ }
9
+
10
+ /* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
11
+ :host {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ font-size: var(--zen-label-font-size);
15
+ font-weight: var(--zen-label-font-weight);
16
+ color: var(--zen-label-color);
17
+ line-height: 1.4;
18
+ user-select: none;
19
+
20
+ &[data-size='sm'] {
21
+ --zen-label-font-size: 0.75rem;
22
+ }
23
+
24
+ &[data-size='md'] {
25
+ --zen-label-font-size: 0.875rem;
26
+ }
27
+
28
+ &[data-size='lg'] {
29
+ --zen-label-font-size: 1rem;
30
+ }
31
+
32
+ &[data-disabled] {
33
+ opacity: $disabled-opacity;
34
+ cursor: not-allowed;
35
+
36
+ label {
37
+ cursor: not-allowed;
38
+ }
39
+ }
40
+ }
41
+
42
+ label {
43
+ cursor: pointer;
44
+ display: inline-flex;
45
+ align-items: center;
46
+ }
47
+
48
+ .required {
49
+ color: var(--zen-label-required-color);
50
+ margin-inline-start: 0.25rem;
51
+ font-weight: 600;
52
+ }
@@ -0,0 +1,77 @@
1
+ import { Component, provideZonelessChangeDetection } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+
5
+ import { ZenLabel } from './label';
6
+
7
+ @Component({
8
+ imports: [ZenLabel],
9
+ template: '<zen-label [for]="targetId" [required]="required">User Label</zen-label>',
10
+ })
11
+ class TestHostComponent {
12
+ targetId = 'input-1';
13
+ required = false;
14
+ }
15
+
16
+ describe('ZenLabel', () => {
17
+ beforeEach(async () => {
18
+ await TestBed.configureTestingModule({
19
+ imports: [ZenLabel, TestHostComponent],
20
+ providers: [provideZonelessChangeDetection()],
21
+ }).compileComponents();
22
+ });
23
+
24
+ it('should create', () => {
25
+ const fixture = TestBed.createComponent(ZenLabel);
26
+ const component = fixture.componentInstance;
27
+ expect(component).toBeTruthy();
28
+ });
29
+
30
+ it('should render label with for attribute matching targetId', () => {
31
+ const fixture = TestBed.createComponent(TestHostComponent);
32
+ fixture.detectChanges();
33
+
34
+ const labelEl = fixture.nativeElement.querySelector('label') as HTMLLabelElement;
35
+ expect(labelEl).toBeTruthy();
36
+ expect(labelEl.getAttribute('for')).toBe('input-1');
37
+ expect(labelEl.textContent?.trim()).toBe('User Label');
38
+ });
39
+
40
+ it('should support id input as fallback for for attribute', () => {
41
+ const fixture = TestBed.createComponent(ZenLabel);
42
+ fixture.componentRef.setInput('id', 'input-fallback');
43
+ fixture.detectChanges();
44
+
45
+ const labelEl = fixture.nativeElement.querySelector('label') as HTMLLabelElement;
46
+ expect(labelEl.getAttribute('for')).toBe('input-fallback');
47
+ });
48
+
49
+ it('should render asterisk when required is true', () => {
50
+ const fixture = TestBed.createComponent(TestHostComponent);
51
+ fixture.componentInstance.required = true;
52
+ fixture.detectChanges();
53
+
54
+ const requiredEl = fixture.nativeElement.querySelector('.required') as HTMLElement;
55
+ expect(requiredEl).toBeTruthy();
56
+ expect(requiredEl.textContent?.trim()).toBe('*');
57
+ });
58
+
59
+ it('should set size attribute', () => {
60
+ const fixture = TestBed.createComponent(ZenLabel);
61
+ for (const size of ['sm', 'md', 'lg'] as const) {
62
+ fixture.componentRef.setInput('size', size);
63
+ fixture.detectChanges();
64
+ const el = fixture.nativeElement as HTMLElement;
65
+ expect(el.getAttribute('data-size')).toBe(size);
66
+ }
67
+ });
68
+
69
+ it('should apply data-disabled when disabled is true', () => {
70
+ const fixture = TestBed.createComponent(ZenLabel);
71
+ fixture.componentRef.setInput('disabled', true);
72
+ fixture.detectChanges();
73
+
74
+ const el = fixture.nativeElement as HTMLElement;
75
+ expect(el.hasAttribute('data-disabled')).toBe(true);
76
+ });
77
+ });
@@ -0,0 +1,67 @@
1
+ import { Meta, StoryObj } from '@storybook/angular';
2
+
3
+ import { ZenLabel } from './label';
4
+
5
+ type Options = ZenLabel;
6
+
7
+ export default {
8
+ title: 'Ui/Label',
9
+ component: ZenLabel,
10
+ argTypes: {
11
+ size: {
12
+ control: 'select',
13
+ options: ['sm', 'md', 'lg'],
14
+ },
15
+ required: {
16
+ control: 'boolean',
17
+ },
18
+ disabled: {
19
+ control: 'boolean',
20
+ },
21
+ for: {
22
+ control: 'text',
23
+ },
24
+ },
25
+ args: {
26
+ size: 'md',
27
+ required: false,
28
+ disabled: false,
29
+ for: 'example-control',
30
+ },
31
+ render: args => ({
32
+ props: args,
33
+ template: `
34
+ <zen-label [disabled]="disabled" [for]="for" [required]="required" [size]="size">
35
+ Label text
36
+ </zen-label>
37
+ `,
38
+ }),
39
+ } satisfies Meta<Options>;
40
+
41
+ type Story = StoryObj<Options>;
42
+
43
+ export const Default: Story = {};
44
+
45
+ export const Required: Story = {
46
+ args: {
47
+ required: true,
48
+ },
49
+ };
50
+
51
+ export const Small: Story = {
52
+ args: {
53
+ size: 'sm',
54
+ },
55
+ };
56
+
57
+ export const Large: Story = {
58
+ args: {
59
+ size: 'lg',
60
+ },
61
+ };
62
+
63
+ export const Disabled: Story = {
64
+ args: {
65
+ disabled: true,
66
+ },
67
+ };
@@ -0,0 +1,57 @@
1
+ import { booleanAttribute, Component, computed, input } from '@angular/core';
2
+
3
+ export type ZenLabelSize = 'sm' | 'md' | 'lg';
4
+
5
+ /**
6
+ * ZenLabel is an accessible label component that links to form controls,
7
+ * supports required indicators (*), multiple sizes, and disabled states.
8
+ *
9
+ * @example
10
+ * ```html
11
+ * <zen-label [for]="id()" required>Email</zen-label>
12
+ * <zen-label for="terms" size="sm">I accept the terms</zen-label>
13
+ * ```
14
+ *
15
+ * ### CSS Custom Properties
16
+ *
17
+ * You can customize the component using CSS custom properties:
18
+ *
19
+ * ```css
20
+ * :root {
21
+ * --zen-label-font-size: 0.875rem;
22
+ * --zen-label-font-weight: 500;
23
+ * --zen-label-color: hsl(0deg 0% 15%);
24
+ * --zen-label-required-color: var(--zen-danger, hsl(348deg 52% 42%));
25
+ * }
26
+ * ```
27
+ *
28
+ * @author Konrad Stępień
29
+ * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
30
+ * @see [GitHub](https://github.com/kstepien3/ng-zen)
31
+ */
32
+ @Component({
33
+ selector: 'zen-label',
34
+ templateUrl: './label.html',
35
+ styleUrl: './label.scss',
36
+ host: {
37
+ '[attr.data-size]': 'size()',
38
+ '[attr.data-disabled]': 'disabled() ? "" : null',
39
+ },
40
+ })
41
+ export class ZenLabel {
42
+ /** The id of the form control this label is linked to. Supports both [for] and [id]. */
43
+ readonly for = input<string>('');
44
+ readonly id = input<string>('');
45
+
46
+ /** Resolved target control id (prioritizes `for`, falls back to `id`). */
47
+ protected readonly targetId = computed(() => this.for() || this.id());
48
+
49
+ /** Whether the associated control is required. Renders a red asterisk (*) when true. */
50
+ readonly required = input(false, { transform: booleanAttribute });
51
+
52
+ /** Visual size variant: 'sm' | 'md' | 'lg'. Defaults to 'md'. */
53
+ readonly size = input<ZenLabelSize>('md');
54
+
55
+ /** Whether the label is in a disabled state. */
56
+ readonly disabled = input(false, { transform: booleanAttribute });
57
+ }
@@ -0,0 +1,34 @@
1
+ <input
2
+ #inputElement
3
+ type="radio"
4
+ [attr.aria-checked]="checked()"
5
+ [attr.aria-disabled]="disabled()"
6
+ [attr.aria-invalid]="invalid() || null"
7
+ [checked]="checked()"
8
+ [disabled]="disabled()"
9
+ [id]="id()"
10
+ [name]="name()"
11
+ [value]="option()"
12
+ (change)="onRadioChange()"
13
+ />
14
+ @if (checked()) {
15
+ <span class="radio-dot"></span>
16
+ }
17
+ @if (label()) {
18
+ <zen-label [disabled]="disabled()" [for]="id()" [required]="required()">{{ label() }}</zen-label>
19
+ }
20
+ @if (shouldShowError()) {
21
+ @for (error of errors(); track $index) {
22
+ <zen-hint severity="error">
23
+ @if (errorTemplate()) {
24
+ <ng-container *ngTemplateOutlet="errorTemplate()!; context: { $implicit: error }" />
25
+ } @else {
26
+ {{ error.message }}
27
+ }
28
+ </zen-hint>
29
+ }
30
+ } @else if (warn()) {
31
+ <zen-hint severity="warning">{{ warn() }}</zen-hint>
32
+ } @else if (hint()) {
33
+ <zen-hint>{{ hint() }}</zen-hint>
34
+ }