@ng-zen/cli 22.0.0 → 22.1.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +2 -16
  2. package/README.md +18 -17
  3. package/package.json +3 -3
  4. package/schematics/ui/files/alert/alert.scss +1 -1
  5. package/schematics/ui/files/alert/alert.ts +1 -2
  6. package/schematics/ui/files/avatar/avatar.ts +1 -2
  7. package/schematics/ui/files/button/button.ts +1 -2
  8. package/schematics/ui/files/card/card.scss +57 -0
  9. package/schematics/ui/files/card/card.spec.ts +20 -0
  10. package/schematics/ui/files/card/card.stories.ts +157 -0
  11. package/schematics/ui/files/card/card.ts +64 -0
  12. package/schematics/ui/files/card/index.ts +1 -0
  13. package/schematics/ui/files/checkbox/checkbox.stories.ts +59 -33
  14. package/schematics/ui/files/checkbox/checkbox.ts +22 -26
  15. package/schematics/ui/files/dialog/dialog.ts +1 -2
  16. package/schematics/ui/files/divider/divider.ts +1 -2
  17. package/schematics/ui/files/form-control/form-control.spec.ts +20 -5
  18. package/schematics/ui/files/form-control/form-control.stories.ts +58 -36
  19. package/schematics/ui/files/form-control/form-control.ts +55 -70
  20. package/schematics/ui/files/icon/icon.ts +1 -2
  21. package/schematics/ui/files/input/input.stories.ts +84 -34
  22. package/schematics/ui/files/input/input.ts +27 -24
  23. package/schematics/ui/files/popover/host/popover-host.ts +1 -2
  24. package/schematics/ui/files/radio/radio.registry.ts +2 -2
  25. package/schematics/ui/files/radio/radio.spec.ts +3 -72
  26. package/schematics/ui/files/radio/radio.stories.ts +60 -112
  27. package/schematics/ui/files/radio/radio.ts +21 -33
  28. package/schematics/ui/files/skeleton/skeleton.ts +1 -2
  29. package/schematics/ui/files/switch/switch.stories.ts +46 -30
  30. package/schematics/ui/files/switch/switch.ts +20 -20
  31. package/schematics/ui/files/textarea/textarea.ts +1 -2
  32. package/schematics/ui/index.js +17 -2
  33. package/schematics/ui/index.js.map +1 -1
  34. package/schematics/ui/index.ts +26 -4
  35. package/schematics/ui/schema.js.map +1 -1
  36. package/schematics/ui/schema.json +1 -1
  37. package/schematics/ui/schema.ts +9 -1
  38. package/utils/apply-file-template.util.js.map +1 -1
  39. package/utils/apply-file-template.util.ts +2 -2
  40. package/types/files-config.js +0 -3
  41. package/types/files-config.js.map +0 -1
  42. package/types/files-config.ts +0 -7
  43. package/types/generator-schema-base.js +0 -3
  44. package/types/generator-schema-base.js.map +0 -1
  45. package/types/generator-schema-base.ts +0 -8
  46. package/types/index.js +0 -7
  47. package/types/index.js.map +0 -1
  48. package/types/index.ts +0 -3
  49. package/types/schematics-folder.js +0 -3
  50. package/types/schematics-folder.js.map +0 -1
  51. package/types/schematics-folder.ts +0 -3
@@ -1,6 +1,5 @@
1
1
  import { provideZonelessChangeDetection } from '@angular/core';
2
2
  import { ComponentFixture, TestBed } from '@angular/core/testing';
3
- import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
4
3
  import { By } from '@angular/platform-browser';
5
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
6
5
 
@@ -14,7 +13,7 @@ describe('ZenRadio', () => {
14
13
 
15
14
  beforeEach(async () => {
16
15
  await TestBed.configureTestingModule({
17
- imports: [ZenRadio, FormsModule, ReactiveFormsModule],
16
+ imports: [ZenRadio],
18
17
  providers: [ZenRadioRegistry, provideZonelessChangeDetection()],
19
18
  }).compileComponents();
20
19
 
@@ -73,7 +72,7 @@ describe('ZenRadio', () => {
73
72
  });
74
73
 
75
74
  it('should not call onInput when disabled', () => {
76
- component.disabled.set(true);
75
+ fixture.componentRef.setInput('disabled', true);
77
76
  fixture.detectChanges();
78
77
 
79
78
  const spy = vi.spyOn(component, 'onInput');
@@ -85,63 +84,8 @@ describe('ZenRadio', () => {
85
84
  expect(spy).not.toHaveBeenCalled();
86
85
  });
87
86
 
88
- it('should integrate with ngModel', () => {
89
- let selectedValue: string | null = null;
90
-
91
- const testFixture = TestBed.createComponent(ZenRadio);
92
- testFixture.componentRef.setInput('name', 'ng-model-group');
93
- testFixture.componentRef.setInput('option', 'option1');
94
- testFixture.detectChanges();
95
-
96
- // Simulate ngModel binding
97
- testFixture.componentInstance.registerOnChange((value: string | null) => {
98
- selectedValue = value;
99
- });
100
-
101
- const inputElement = testFixture.debugElement.query(By.css('input')).nativeElement;
102
- inputElement.checked = true;
103
- inputElement.dispatchEvent(new Event('change'));
104
-
105
- expect(selectedValue).toBe('option1');
106
- });
107
-
108
- it('should integrate with reactive forms', () => {
109
- const formControl = new FormControl<string | null>(null);
110
- const testFixture = TestBed.createComponent(ZenRadio);
111
- testFixture.componentRef.setInput('name', 'reactive-group');
112
- testFixture.componentRef.setInput('option', 'option1');
113
-
114
- // Simulate form control binding
115
- testFixture.componentInstance.writeValue(formControl.value);
116
-
117
- testFixture.componentInstance.registerOnChange((value: string | null) => {
118
- formControl.setValue(value);
119
- });
120
-
121
- testFixture.detectChanges();
122
-
123
- const inputElement = testFixture.debugElement.query(By.css('input')).nativeElement;
124
- inputElement.checked = true;
125
- inputElement.dispatchEvent(new Event('change'));
126
-
127
- // The component's onInput method should call the onChange callback
128
- // which should update form control value
129
- expect(formControl.value).toBe('option1');
130
- });
131
-
132
- it('should handle disabled state from form control', () => {
133
- component.setDisabledState(true);
134
- fixture.detectChanges();
135
-
136
- expect(component.disabled()).toBe(true);
137
-
138
- const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
139
- expect(inputElement.disabled).toBe(true);
140
- });
141
-
142
87
  it('should have correct ARIA attributes', () => {
143
88
  component.value.set('test-value');
144
- component.disabled.set(false);
145
89
  fixture.detectChanges();
146
90
 
147
91
  const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
@@ -151,7 +95,7 @@ describe('ZenRadio', () => {
151
95
 
152
96
  it('should update ARIA attributes when state changes', () => {
153
97
  component.value.set('different-value');
154
- component.disabled.set(true);
98
+ fixture.componentRef.setInput('disabled', true);
155
99
  fixture.detectChanges();
156
100
 
157
101
  const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
@@ -159,24 +103,12 @@ describe('ZenRadio', () => {
159
103
  expect(inputElement.getAttribute('aria-disabled')).toBe('true');
160
104
  });
161
105
 
162
- it('should call onTouched when host element loses focus', () => {
163
- const onTouchedSpy = vi.fn();
164
- component.registerOnTouched(onTouchedSpy);
165
-
166
- const hostElement = fixture.debugElement.nativeElement;
167
- hostElement.dispatchEvent(new Event('blur'));
168
-
169
- expect(onTouchedSpy).toHaveBeenCalled();
170
- });
171
-
172
106
  it('should register with radio registry on initialization', () => {
173
- // Create a new radio to test registry registration
174
107
  const newFixture = TestBed.createComponent(ZenRadio);
175
108
  newFixture.componentRef.setInput('name', 'registry-test');
176
109
  newFixture.componentRef.setInput('option', 'registry-option');
177
110
  newFixture.detectChanges();
178
111
 
179
- // The radio should be registered after next render
180
112
  expect(newFixture.componentInstance).toBeTruthy();
181
113
  });
182
114
 
@@ -190,7 +122,6 @@ describe('ZenRadio', () => {
190
122
 
191
123
  const radio2 = radio2Fixture.componentInstance;
192
124
 
193
- // Test registry selection
194
125
  registry.select('test-group', 'option2');
195
126
 
196
127
  expect(radio1.value()).toBe('option2');
@@ -1,87 +1,53 @@
1
- import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
1
+ import { NgComponentOutlet } from '@angular/common';
2
+ import { Component, signal } from '@angular/core';
3
+ import { form, FormField } from '@angular/forms/signals';
2
4
  import { Meta, StoryObj } from '@storybook/angular';
3
5
 
6
+ import FormControlStories from '../form-control/form-control.stories';
4
7
  import { ZenRadio } from './radio';
5
8
 
6
- interface StoryParams {
7
- selectedColor?: string;
8
- }
9
-
10
- type Options = ZenRadio & StoryParams;
9
+ type Options = ZenRadio;
11
10
 
12
11
  export default {
13
12
  title: 'Ui/Radio',
14
13
  component: ZenRadio,
15
14
  argTypes: {
15
+ ...FormControlStories.argTypes,
16
16
  value: {
17
17
  table: {
18
18
  category: 'models',
19
- type: {
20
- summary: 'string | null',
21
- },
22
- defaultValue: {
23
- summary: 'null',
24
- },
19
+ type: { summary: 'string | null' },
20
+ defaultValue: { summary: 'null' },
25
21
  },
26
- control: 'text',
22
+ control: 'text' as const,
27
23
  },
28
24
  name: {
29
25
  table: {
30
26
  category: 'models',
31
- type: {
32
- summary: 'string',
33
- },
34
- defaultValue: {
35
- summary: 'radio',
36
- },
27
+ type: { summary: 'string' },
28
+ defaultValue: { summary: 'radio' },
37
29
  },
38
- control: 'text',
30
+ control: 'text' as const,
39
31
  },
40
32
  option: {
41
33
  table: {
42
34
  category: 'inputs',
43
- type: {
44
- summary: 'string',
45
- },
46
- },
47
- control: 'text',
48
- },
49
- disabled: {
50
- control: 'boolean',
51
- table: {
52
- category: 'models',
53
- type: {
54
- summary: 'boolean',
55
- },
56
- },
57
- },
58
- required: {
59
- control: 'boolean',
60
- table: {
61
- category: 'inputs',
62
- type: {
63
- summary: 'boolean',
64
- },
65
- defaultValue: {
66
- summary: 'false',
67
- },
35
+ type: { summary: 'string' },
68
36
  },
37
+ control: 'text' as const,
69
38
  },
70
39
  onInput: {
71
40
  table: {
72
41
  readonly: true,
73
- type: {
74
- summary: '(value: string | null) => void',
75
- },
42
+ type: { summary: '(value: string | null) => void' },
76
43
  },
77
44
  },
78
45
  },
79
46
  args: {
47
+ ...FormControlStories.args,
80
48
  value: null,
81
49
  name: 'radio-group',
82
50
  option: 'option1',
83
- disabled: false,
84
- required: false,
85
51
  },
86
52
  } satisfies Meta<Options>;
87
53
 
@@ -100,70 +66,52 @@ export const WithLabel: Story = {
100
66
  }),
101
67
  };
102
68
 
103
- export const NgModel: Story = {
69
+ export const WithSignalForm: Story = {
104
70
  render: () => ({
105
- moduleMetadata: {
106
- imports: [FormsModule],
107
- },
108
- props: {
109
- selectedColor: 'blue',
110
- },
111
- template: `
112
- <div style="display: flex; flex-direction: column; gap: 1rem">
113
- <div>
114
- <strong>Selected value:</strong>
115
- <p> {{ selectedColor || 'None' }}</p>
116
- </div>
117
- <div style="display: flex; flex-direction: column; gap: 0.5rem">
118
- <div style="display: flex; align-items: center; gap: 0.25rem">
119
- <zen-radio name="color-group" option="red" [(ngModel)]="selectedColor" />
120
- <label>Red</label>
121
- </div>
122
- <div style="display: flex; align-items: center; gap: 0.25rem">
123
- <zen-radio name="color-group" option="green" [(ngModel)]="selectedColor" />
124
- <label>Green</label>
125
- </div>
126
- <div style="display: flex; align-items: center; gap: 0.25rem">
127
- <zen-radio name="color-group" option="blue" [(ngModel)]="selectedColor" />
128
- <label>Blue</label>
129
- </div>
130
- </div>
131
- </div>
132
- `,
71
+ moduleMetadata: { imports: [NgComponentOutlet] },
72
+ props: { component: RadioSignalFormComponent },
73
+ template: '<ng-container *ngComponentOutlet="component" />',
133
74
  }),
134
- };
135
-
136
- export const AsFromControl: Story = {
137
- render: () => {
138
- return {
139
- moduleMetadata: {
140
- imports: [ReactiveFormsModule],
141
- },
142
- props: {
143
- colorControl: new FormControl('green'),
75
+ parameters: {
76
+ docs: {
77
+ source: {
78
+ code: `<zen-radio [formField]="form.color" option="red" />
79
+ <zen-radio [formField]="form.color" option="green" />
80
+ <zen-radio [formField]="form.color" option="blue" />`,
144
81
  },
145
- template: `
146
- <div style="display: flex; flex-direction: column; gap: 1rem">
147
- <div>
148
- <strong>Selected value:</strong>
149
- <p> {{ colorControl.value || 'None' }}</p>
150
- </div>
151
- <div style="display: flex; flex-direction: column; gap: 0.5rem">
152
- <div style="display: flex; align-items: center; gap: 0.25rem">
153
- <zen-radio name="working-group" option="red" [formControl]="colorControl" />
154
- <label>Red</label>
155
- </div>
156
- <div style="display: flex; align-items: center; gap: 0.25rem">
157
- <zen-radio name="working-group" option="green" [formControl]="colorControl" />
158
- <label>Green</label>
159
- </div>
160
- <div style="display: flex; align-items: center; gap: 0.25rem">
161
- <zen-radio name="working-group" option="blue" [formControl]="colorControl" />
162
- <label>Blue</label>
163
- </div>
164
- </div>
165
- </div>
166
- `,
167
- };
82
+ },
168
83
  },
169
84
  };
85
+
86
+ @Component({
87
+ standalone: true,
88
+ imports: [FormField, ZenRadio],
89
+ template: `
90
+ <div style="display: flex; flex-direction: column; gap: 1rem">
91
+ <div>
92
+ <strong>Selected:</strong>
93
+ {{ form.color().value() }}
94
+ </div>
95
+ <div style="display: flex; flex-direction: column; gap: 0.5rem">
96
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -->
97
+ <label style="display: flex; align-items: center; gap: 0.25rem">
98
+ <zen-radio [formField]="form.color" option="red" />
99
+ Red
100
+ </label>
101
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -->
102
+ <label style="display: flex; align-items: center; gap: 0.25rem">
103
+ <zen-radio [formField]="form.color" option="green" />
104
+ Green
105
+ </label>
106
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -->
107
+ <label style="display: flex; align-items: center; gap: 0.25rem">
108
+ <zen-radio [formField]="form.color" option="blue" />
109
+ Blue
110
+ </label>
111
+ </div>
112
+ </div>
113
+ `,
114
+ })
115
+ class RadioSignalFormComponent {
116
+ readonly form = form(signal({ color: 'blue' }));
117
+ }
@@ -1,43 +1,36 @@
1
- import {
2
- afterNextRender,
3
- ChangeDetectionStrategy,
4
- Component,
5
- computed,
6
- DestroyRef,
7
- inject,
8
- input,
9
- model,
10
- } from '@angular/core';
11
- import { FormsModule } from '@angular/forms';
1
+ import { afterNextRender, Component, computed, DestroyRef, inject, input, model } from '@angular/core';
12
2
 
13
- import { ZenFormControl, ZenFormControlProvider } from '../form-control';
3
+ import { ZenFormControl } from '../form-control';
14
4
  import { ZenRadioRegistry } from './radio.registry';
15
5
 
16
6
  /**
17
- * ZenRadio is a reusable radio button component designed to provide
18
- * a consistent and customizable radio button style across the application.
19
- * It supports Angular forms integration and provides two-way data binding
20
- * for string values.
7
+ * ZenRadio is a radio button component backed by Signal Forms.
21
8
  *
22
- * @example
23
- * <zen-radio name="group" option="option1" [(ngModel)]="selectedValue" /> Option 1
24
- * <zen-radio name="group" option="option2" [(ngModel)]="selectedValue" /> Option 2
9
+ * Radio buttons with the same `name` form a group. The `value` is shared
10
+ * across the group — when one radio is selected, `onInput(value)` is called
11
+ * on every radio in the group via {@link ZenRadioRegistry}.
12
+ *
13
+ * Use together with a shared `[formField]` binding:
14
+ *
15
+ * ```html
16
+ * <zen-radio [formField]="form.color" name="color" option="red" />
17
+ * <zen-radio [formField]="form.color" name="color" option="green" />
18
+ * ```
25
19
  *
26
20
  * ### CSS Custom Properties
27
21
  *
28
- * You can customize the component using CSS custom properties:
29
22
  *
30
23
  * ```css
31
24
  * :root {
32
- * --zen-radio-size: 1rem;
33
- * --zen-radio-border-radius: 50%;
34
- * --zen-radio-appearance: hsl(0deg 0% 10%);
35
- * --zen-radio-disabled-opacity: 0.6;
36
- * --zen-radio-border: 1px solid hsl(0deg 0% 80%);
25
+ * --zen-radio-size: 1rem;
26
+ * --zen-radio-border-radius: 50%;
27
+ * --zen-radio-appearance: hsl(0deg 0% 10%);
28
+ * --zen-radio-disabled-opacity: 0.6;
29
+ * --zen-radio-border: 1px solid hsl(0deg 0% 80%);
37
30
  * }
38
31
  * ```
39
32
  *
40
- * @implements {ZenFormControl<string | null>}
33
+ * @extends {ZenFormControl<string | null>}
41
34
  *
42
35
  * @author Konrad Stępień
43
36
  * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
@@ -49,6 +42,7 @@ import { ZenRadioRegistry } from './radio.registry';
49
42
  <input
50
43
  [attr.aria-checked]="checked()"
51
44
  [attr.aria-disabled]="disabled()"
45
+ [attr.aria-invalid]="invalid() || null"
52
46
  [checked]="checked()"
53
47
  [disabled]="disabled()"
54
48
  [name]="name()"
@@ -62,12 +56,6 @@ import { ZenRadioRegistry } from './radio.registry';
62
56
  }
63
57
  `,
64
58
  styleUrls: ['./radio.scss'],
65
- changeDetection: ChangeDetectionStrategy.OnPush,
66
- imports: [FormsModule],
67
- providers: [ZenFormControlProvider(ZenRadio)],
68
- host: {
69
- '(blur)': 'onTouched()',
70
- },
71
59
  })
72
60
  export class ZenRadio extends ZenFormControl<string | null> {
73
61
  /**
@@ -80,7 +68,7 @@ export class ZenRadio extends ZenFormControl<string | null> {
80
68
  * The name attribute for the radio button group.
81
69
  * Radio buttons with the same name will be grouped together.
82
70
  */
83
- readonly name = input.required<string>();
71
+ override readonly name = input.required<string>();
84
72
 
85
73
  /**
86
74
  * The value for this specific radio button.
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component } from '@angular/core';
1
+ import { Component } from '@angular/core';
2
2
 
3
3
  /**
4
4
  * ZenInput loader component that displays loading states.
@@ -31,6 +31,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
31
31
  selector: 'zen-skeleton, zen-skeleton[rounded]',
32
32
  template: ``,
33
33
  styleUrl: './skeleton.scss',
34
- changeDetection: ChangeDetectionStrategy.OnPush,
35
34
  })
36
35
  export class ZenSkeleton {}
@@ -1,5 +1,9 @@
1
+ import { NgComponentOutlet } from '@angular/common';
2
+ import { Component, signal } from '@angular/core';
3
+ import { form, FormField } from '@angular/forms/signals';
1
4
  import { Meta, StoryObj } from '@storybook/angular';
2
5
 
6
+ import FormControlStories from '../form-control/form-control.stories';
3
7
  import { ZenSwitch } from './switch';
4
8
 
5
9
  type Options = ZenSwitch;
@@ -8,42 +12,18 @@ export default {
8
12
  title: 'Ui/Switch',
9
13
  component: ZenSwitch,
10
14
  argTypes: {
15
+ ...FormControlStories.argTypes,
11
16
  value: {
12
- control: 'boolean',
17
+ control: 'boolean' as const,
13
18
  table: {
14
19
  category: 'models',
15
- type: {
16
- summary: 'boolean',
17
- },
18
- },
19
- },
20
- disabled: {
21
- control: 'boolean',
22
- table: {
23
- category: 'models',
24
- type: {
25
- summary: 'boolean',
26
- },
27
- },
28
- },
29
- required: {
30
- control: 'boolean',
31
- table: {
32
- category: 'inputs',
33
- type: {
34
- summary: 'boolean',
35
- },
36
- defaultValue: {
37
- summary: 'false',
38
- },
20
+ type: { summary: 'boolean' },
39
21
  },
40
22
  },
41
23
  onInput: {
42
24
  table: {
43
25
  readonly: true,
44
- type: {
45
- summary: '(value: boolean) => void',
46
- },
26
+ type: { summary: '(value: boolean) => void' },
47
27
  },
48
28
  },
49
29
  onKeyDown: {
@@ -53,12 +33,48 @@ export default {
53
33
  },
54
34
  },
55
35
  args: {
36
+ ...FormControlStories.args,
56
37
  value: false,
57
- disabled: false,
58
- required: false,
59
38
  },
60
39
  } satisfies Meta<Options>;
61
40
 
62
41
  type Story = StoryObj<Options>;
63
42
 
64
43
  export const Default: Story = {};
44
+
45
+ export const WithSignalForm: Story = {
46
+ render: () => ({
47
+ moduleMetadata: { imports: [NgComponentOutlet] },
48
+ props: { component: SwitchSignalFormComponent },
49
+ template: '<ng-container *ngComponentOutlet="component" />',
50
+ }),
51
+ parameters: {
52
+ docs: {
53
+ source: {
54
+ code: `
55
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
56
+ <zen-switch id="notifications" [formField]="form.notifications" />
57
+ <label for="notifications">
58
+ {{ form.notifications().value() ? 'Notifications on' : 'Notifications off' }}
59
+ </label>
60
+ </div>`,
61
+ },
62
+ },
63
+ },
64
+ };
65
+
66
+ @Component({
67
+ standalone: true,
68
+ template: `
69
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
70
+ <zen-switch [formField]="form.notifications" id="notifications" />
71
+ <label for="notifications">
72
+ {{ form.notifications().value() ? 'Notifications on' : 'Notifications off' }}
73
+ </label>
74
+ </div>
75
+ `,
76
+ imports: [FormField, ZenSwitch],
77
+ })
78
+ class SwitchSignalFormComponent {
79
+ readonly form = form(signal({ notifications: false }));
80
+ }
@@ -1,34 +1,36 @@
1
- import { ChangeDetectionStrategy, Component, model } from '@angular/core';
1
+ import { Component, model } from '@angular/core';
2
2
 
3
- import { ZenFormControl, ZenFormControlProvider } from '../form-control';
3
+ import { ZenFormControl } from '../form-control';
4
4
 
5
5
  /**
6
- * ZenInput is a reusable text input component designed to provide
7
- * a consistent and customizable input style across the application.
8
- * It supports Angular forms integration and provides two-way data binding.
6
+ * ZenSwitch is a toggle switch component for boolean values backed by
7
+ * Signal Forms.
9
8
  *
10
- * @example
9
+ * Connect it to a Signal Forms field with `[formField]`:
11
10
  *
12
11
  * ```html
13
- * <zen-switch [disabled]="false" [value]="true" />
14
- *```
12
+ * <zen-switch [formField]="myForm.notifications" />
13
+ * ```
14
+ *
15
+ * Supports keyboard interaction: `Enter`, `Space`, `ArrowRight`, and
16
+ * `ArrowLeft` keys toggle or set the value.
15
17
  *
16
18
  * ### CSS Custom Properties
17
- * You can customize the component using CSS custom properties:
19
+ *
18
20
  *
19
21
  * ```css
20
22
  * :root {
21
- * --zen-input-border: 1px solid hsl(0deg 0% 80%);
22
- * --zen-input-border-radius: 8px;
23
- * --zen-input-padding: 0.5rem 1rem;
24
- * --zen-input-focus-shadow: 0 1px 4px hsl(0deg 0% 60% / 20%) inset;
25
- * --zen-input-placeholder-color: hsl(0deg 0% 60%);
23
+ * --zen-switch-thumb-size: 1rem;
24
+ * --zen-switch-height: 1.25rem;
25
+ * --zen-switch-width: 2rem;
26
+ * --zen-switch-apperance: hsl(0deg 0% 10%);
27
+ * --zen-switch-background: hsl(0deg 0% 80%);
26
28
  * }
27
29
  * ```
28
30
  *
29
- * @implements {ZenFormControl<boolean>}
31
+ * @extends {ZenFormControl<boolean>}
30
32
  *
31
- * @author Konrad Stępień
33
+ * @author Konrad Stepień
32
34
  * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
33
35
  * @see [GitHub](https://github.com/kstepien3/ng-zen)
34
36
  */
@@ -41,20 +43,18 @@ import { ZenFormControl, ZenFormControlProvider } from '../form-control';
41
43
  </span>
42
44
  `,
43
45
  styleUrl: './switch.scss',
44
- changeDetection: ChangeDetectionStrategy.OnPush,
45
- providers: [ZenFormControlProvider(ZenSwitch)],
46
+
46
47
  host: {
47
48
  tabindex: '0',
48
49
  role: 'switch',
49
50
  '[attr.data-disabled]': 'disabled()',
50
51
  '[attr.data-checked]': 'value()',
51
- '(blur)': 'onTouched()',
52
52
  '(click)': 'onInput(!this.value())',
53
53
  '(keydown)': 'onKeyDown($event)',
54
54
  },
55
55
  })
56
56
  export class ZenSwitch extends ZenFormControl<boolean> {
57
- /** Holds the current checkbox value. */
57
+ /** Holds the current switch value. */
58
58
  readonly value = model(false);
59
59
 
60
60
  /**
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component } from '@angular/core';
1
+ import { Component } from '@angular/core';
2
2
 
3
3
  /**
4
4
  * ZenTextarea is a reusable textarea component designed to provide
@@ -32,6 +32,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
32
32
  <ng-content />
33
33
  `,
34
34
  styleUrls: ['./textarea.scss'],
35
- changeDetection: ChangeDetectionStrategy.OnPush,
36
35
  })
37
36
  export class ZenTextarea {}
@@ -6,8 +6,21 @@ const schematics_1 = require("@angular-devkit/schematics");
6
6
  const workspace_1 = require("@schematics/angular/utility/workspace");
7
7
  const utils_1 = require("../../utils");
8
8
  const DEFAULT_GENERATION_PATH = 'ui'; // src/app/ui
9
- function uiGenerator({ ui, project, ...options }) {
10
- return async (tree) => {
9
+ const FORM_CONTROL_DEPS = new Set(['input', 'checkbox', 'switch', 'radio']);
10
+ function resolveFormControlDependency(tree, logger, ui, workingDirectory) {
11
+ if (!ui.some(u => FORM_CONTROL_DEPS.has(u)))
12
+ return;
13
+ const fcPath = (0, core_1.normalize)(`${workingDirectory}/form-control/form-control.ts`);
14
+ if (tree.exists(fcPath)) {
15
+ logger.info('ℹ form-control skipped — already exists');
16
+ }
17
+ else {
18
+ ui.push('form-control');
19
+ logger.info('✔ form-control included (auto-included for form components)');
20
+ }
21
+ }
22
+ function uiGenerator({ ui: selected, project, ...options }) {
23
+ return async (tree, context) => {
11
24
  const workspace = await (0, workspace_1.getWorkspace)(tree);
12
25
  const projectName = project || workspace.extensions['defaultProject'];
13
26
  const projectObj = workspace.projects.get(projectName);
@@ -18,6 +31,8 @@ function uiGenerator({ ui, project, ...options }) {
18
31
  options.path = ((0, workspace_1.buildDefaultPath)(projectObj) + '/' + DEFAULT_GENERATION_PATH);
19
32
  }
20
33
  const workingDirectory = (0, core_1.normalize)((0, core_1.join)(options.currentDirectory, options.path));
34
+ const ui = [...selected];
35
+ resolveFormControlDependency(tree, context.logger, ui, workingDirectory);
21
36
  return (0, schematics_1.chain)([...(0, utils_1.applyFileTemplateUtil)(ui, { ...options, path: workingDirectory })]);
22
37
  };
23
38
  }