@ng-zen/cli 20.4.0 β†’ 20.5.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [20.5.0-next.1](https://github.com/kstepien3/ng-zen/compare/v20.4.0...v20.5.0-next.1) (2025-11-28)
2
+
3
+ ### πŸš€ New Features
4
+
5
+ * **radio:** add component ([#358](https://github.com/kstepien3/ng-zen/issues/358)) ([a2f170e](https://github.com/kstepien3/ng-zen/commit/a2f170e0b18eadfc43c39a1d36597192d0f90c73))
6
+
7
+ ### πŸ“š Documentation
8
+
9
+ * **assets:** optimize logos ([#354](https://github.com/kstepien3/ng-zen/issues/354)) ([66fd5ba](https://github.com/kstepien3/ng-zen/commit/66fd5ba61e005b407c036dd34dcfa996e8884e8b))
10
+ * **development:** fix verdaccio installation command in guide ([#359](https://github.com/kstepien3/ng-zen/issues/359)) ([6945e65](https://github.com/kstepien3/ng-zen/commit/6945e65f648362ec096e17de6a2118be3340cbee))
11
+
1
12
  ## [20.4.0](https://github.com/kstepien3/ng-zen/compare/v20.3.0...v20.4.0) (2025-10-05)
2
13
 
3
14
  ### πŸš€ New Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-zen/cli",
3
- "version": "20.4.0",
3
+ "version": "20.5.0-next.1",
4
4
  "description": "Angular UI components generator – Zen UI Kit CLI for schematics-based creation of customizable components like avatar, button, checkbox, divider, form-control, icon, input, skeleton, switch, textarea with Storybook demos.",
5
5
  "license": "BSD-2-Clause",
6
6
  "private": false,
@@ -0,0 +1 @@
1
+ export * from './radio';
@@ -0,0 +1,34 @@
1
+ import { Injectable } from '@angular/core';
2
+
3
+ import { ZenRadio } from './radio';
4
+
5
+ @Injectable({ providedIn: 'root' })
6
+ export class ZenRadioRegistry {
7
+ private readonly radios = new Map<string, ZenRadio[]>();
8
+
9
+ add(name: string, radio: ZenRadio): void {
10
+ if (!this.radios.has(name)) {
11
+ this.radios.set(name, []);
12
+ }
13
+ this.radios.get(name)!.push(radio);
14
+ }
15
+
16
+ remove(name: string, radio: ZenRadio): void {
17
+ const group = this.radios.get(name);
18
+ if (!group) return;
19
+
20
+ const index = group.indexOf(radio);
21
+ if (index > -1) {
22
+ group.splice(index, 1);
23
+ }
24
+ }
25
+
26
+ select(name: string, value: string): void {
27
+ const group = this.radios.get(name);
28
+ if (!group) return;
29
+
30
+ group.forEach(radio => {
31
+ radio.onInput(value);
32
+ });
33
+ }
34
+ }
@@ -0,0 +1,86 @@
1
+ // Globals
2
+ $error: var(--zen-error, hsl(0deg 70% 50%));
3
+ $focus-shadow: var(--zen-focus-shadow, 0 1px 4px hsl(0deg 0% 60% / 20%) inset);
4
+ $outline: var(--zen-outline, 2px solid hsl(200deg 100% 50% / 50%));
5
+ $transition-duration: var(--zen-transition-duration, 0.2s);
6
+
7
+ :host {
8
+ --zen-radio-size: 1rem;
9
+ --zen-radio-border-radius: 50%;
10
+ --zen-radio-appearance: hsl(0deg 0% 10%);
11
+ --zen-radio-disabled-opacity: 0.6;
12
+ --zen-radio-border: 1px solid hsl(0deg 0% 80%);
13
+ }
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
+ /* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
28
+ :host {
29
+ border: var(--zen-radio-border);
30
+ border-radius: var(--zen-radio-border-radius);
31
+ height: var(--zen-radio-size);
32
+ width: var(--zen-radio-size);
33
+ background-color: white;
34
+ cursor: pointer;
35
+ position: relative;
36
+ transition:
37
+ background-color ease,
38
+ border-color ease;
39
+ transition-duration: $transition-duration;
40
+ user-select: none;
41
+ justify-content: center;
42
+ display: grid;
43
+ place-items: center;
44
+
45
+ &:has(input:checked) {
46
+ border-color: var(--zen-radio-appearance);
47
+ }
48
+
49
+ &:has(input[type='radio']:disabled) {
50
+ opacity: var(--zen-radio-disabled-opacity);
51
+
52
+ &,
53
+ input {
54
+ cursor: not-allowed;
55
+ }
56
+ }
57
+
58
+ &:has(input:focus-visible) {
59
+ outline: $outline;
60
+ box-shadow: $focus-shadow;
61
+ }
62
+
63
+ &:has(input:user-invalid) {
64
+ border-color: $error;
65
+ }
66
+ }
67
+
68
+ .radio-dot {
69
+ width: calc(var(--zen-radio-size) * 0.4);
70
+ height: calc(var(--zen-radio-size) * 0.4);
71
+ background-color: var(--zen-radio-appearance);
72
+ border-radius: 50%;
73
+ animation: radio-dot-enter 0.2s ease-out;
74
+ }
75
+
76
+ @keyframes radio-dot-enter {
77
+ from {
78
+ transform: scale(0);
79
+ opacity: 0;
80
+ }
81
+
82
+ to {
83
+ transform: scale(1);
84
+ opacity: 1;
85
+ }
86
+ }
@@ -0,0 +1,278 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
3
+ import { By } from '@angular/platform-browser';
4
+
5
+ import { ZenRadio } from './radio';
6
+ import { ZenRadioRegistry } from './radio.registry';
7
+
8
+ describe('ZenRadio', () => {
9
+ let component: ZenRadio;
10
+ let fixture: ComponentFixture<ZenRadio>;
11
+ let registry: ZenRadioRegistry;
12
+
13
+ beforeEach(async () => {
14
+ await TestBed.configureTestingModule({
15
+ imports: [ZenRadio, FormsModule, ReactiveFormsModule],
16
+ providers: [ZenRadioRegistry],
17
+ }).compileComponents();
18
+
19
+ registry = TestBed.inject(ZenRadioRegistry);
20
+ fixture = TestBed.createComponent(ZenRadio);
21
+ component = fixture.componentInstance;
22
+ fixture.componentRef.setInput('name', 'test-group');
23
+ fixture.componentRef.setInput('option', 'test-value');
24
+ fixture.detectChanges();
25
+ });
26
+
27
+ it('should create', () => {
28
+ expect(component).toBeTruthy();
29
+ });
30
+
31
+ it('should check when value matches option', () => {
32
+ component.value.set('test-value');
33
+ fixture.detectChanges();
34
+
35
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
36
+ expect(inputElement.checked).toBe(true);
37
+ });
38
+
39
+ it('should uncheck when value differs from option', () => {
40
+ component.value.set('different-value');
41
+ fixture.detectChanges();
42
+
43
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
44
+ expect(inputElement.checked).toBe(false);
45
+ });
46
+
47
+ it('should show radio dot when checked', () => {
48
+ component.value.set('test-value');
49
+ fixture.detectChanges();
50
+
51
+ const radioDot = fixture.debugElement.query(By.css('.radio-dot'));
52
+ expect(radioDot).toBeTruthy();
53
+ });
54
+
55
+ it('should not show radio dot when unchecked', () => {
56
+ component.value.set('different-value');
57
+ fixture.detectChanges();
58
+
59
+ const radioDot = fixture.debugElement.query(By.css('.radio-dot'));
60
+ expect(radioDot).toBeFalsy();
61
+ });
62
+
63
+ it('should call onInput when radio is selected', () => {
64
+ const spy = jest.spyOn(component, 'onInput');
65
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
66
+
67
+ inputElement.checked = true;
68
+ inputElement.dispatchEvent(new Event('change'));
69
+
70
+ expect(spy).toHaveBeenCalledWith('test-value');
71
+ });
72
+
73
+ it('should not call onInput when disabled', () => {
74
+ component.disabled.set(true);
75
+ fixture.detectChanges();
76
+
77
+ const spy = jest.spyOn(component, 'onInput');
78
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
79
+
80
+ inputElement.checked = true;
81
+ inputElement.dispatchEvent(new Event('change'));
82
+
83
+ expect(spy).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it('should integrate with ngModel', () => {
87
+ let selectedValue: string | null = null;
88
+
89
+ const testFixture = TestBed.createComponent(ZenRadio);
90
+ testFixture.componentRef.setInput('name', 'ng-model-group');
91
+ testFixture.componentRef.setInput('option', 'option1');
92
+ testFixture.detectChanges();
93
+
94
+ // Simulate ngModel binding
95
+ testFixture.componentInstance.registerOnChange((value: string | null) => {
96
+ selectedValue = value;
97
+ });
98
+
99
+ const inputElement = testFixture.debugElement.query(By.css('input')).nativeElement;
100
+ inputElement.checked = true;
101
+ inputElement.dispatchEvent(new Event('change'));
102
+
103
+ expect(selectedValue).toBe('option1');
104
+ });
105
+
106
+ it('should integrate with reactive forms', () => {
107
+ const formControl = new FormControl<string | null>(null);
108
+ const testFixture = TestBed.createComponent(ZenRadio);
109
+ testFixture.componentRef.setInput('name', 'reactive-group');
110
+ testFixture.componentRef.setInput('option', 'option1');
111
+
112
+ // Simulate form control binding
113
+ testFixture.componentInstance.writeValue(formControl.value);
114
+
115
+ testFixture.componentInstance.registerOnChange((value: string | null) => {
116
+ formControl.setValue(value);
117
+ });
118
+
119
+ testFixture.detectChanges();
120
+
121
+ const inputElement = testFixture.debugElement.query(By.css('input')).nativeElement;
122
+ inputElement.checked = true;
123
+ inputElement.dispatchEvent(new Event('change'));
124
+
125
+ // The component's onInput method should call the onChange callback
126
+ // which should update form control value
127
+ expect(formControl.value).toBe('option1');
128
+ });
129
+
130
+ it('should handle disabled state from form control', () => {
131
+ component.setDisabledState(true);
132
+ fixture.detectChanges();
133
+
134
+ expect(component.disabled()).toBe(true);
135
+
136
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
137
+ expect(inputElement.disabled).toBe(true);
138
+ });
139
+
140
+ it('should have correct ARIA attributes', () => {
141
+ component.value.set('test-value');
142
+ component.disabled.set(false);
143
+ fixture.detectChanges();
144
+
145
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
146
+ expect(inputElement.getAttribute('aria-checked')).toBe('true');
147
+ expect(inputElement.getAttribute('aria-disabled')).toBe('false');
148
+ });
149
+
150
+ it('should update ARIA attributes when state changes', () => {
151
+ component.value.set('different-value');
152
+ component.disabled.set(true);
153
+ fixture.detectChanges();
154
+
155
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
156
+ expect(inputElement.getAttribute('aria-checked')).toBe('false');
157
+ expect(inputElement.getAttribute('aria-disabled')).toBe('true');
158
+ });
159
+
160
+ it('should call onTouched when host element loses focus', () => {
161
+ const onTouchedSpy = jest.fn();
162
+ component.registerOnTouched(onTouchedSpy);
163
+
164
+ const hostElement = fixture.debugElement.nativeElement;
165
+ hostElement.dispatchEvent(new Event('blur'));
166
+
167
+ expect(onTouchedSpy).toHaveBeenCalled();
168
+ });
169
+
170
+ it('should register with radio registry on initialization', () => {
171
+ // Create a new radio to test registry registration
172
+ const newFixture = TestBed.createComponent(ZenRadio);
173
+ newFixture.componentRef.setInput('name', 'registry-test');
174
+ newFixture.componentRef.setInput('option', 'registry-option');
175
+ newFixture.detectChanges();
176
+
177
+ // The radio should be registered after next render
178
+ expect(newFixture.componentInstance).toBeTruthy();
179
+ });
180
+
181
+ it('should handle registry selection', () => {
182
+ const radio1 = fixture.componentInstance;
183
+
184
+ const radio2Fixture = TestBed.createComponent(ZenRadio);
185
+ radio2Fixture.componentRef.setInput('name', 'test-group');
186
+ radio2Fixture.componentRef.setInput('option', 'option2');
187
+ radio2Fixture.detectChanges();
188
+
189
+ const radio2 = radio2Fixture.componentInstance;
190
+
191
+ // Test registry selection
192
+ registry.select('test-group', 'option2');
193
+
194
+ expect(radio1.value()).toBe('option2');
195
+ expect(radio2.value()).toBe('option2');
196
+ });
197
+ });
198
+
199
+ describe('ZenRadioRegistry', () => {
200
+ let registry: ZenRadioRegistry;
201
+ let radio1: ZenRadio;
202
+ let radio2: ZenRadio;
203
+
204
+ beforeEach(() => {
205
+ TestBed.configureTestingModule({
206
+ imports: [ZenRadio],
207
+ providers: [ZenRadioRegistry],
208
+ });
209
+
210
+ registry = TestBed.inject(ZenRadioRegistry);
211
+
212
+ const fixture1 = TestBed.createComponent(ZenRadio);
213
+ fixture1.componentRef.setInput('name', 'test-group');
214
+ fixture1.componentRef.setInput('option', 'option1');
215
+ radio1 = fixture1.componentInstance;
216
+
217
+ const fixture2 = TestBed.createComponent(ZenRadio);
218
+ fixture2.componentRef.setInput('name', 'test-group');
219
+ fixture2.componentRef.setInput('option', 'option2');
220
+ radio2 = fixture2.componentInstance;
221
+ });
222
+
223
+ it('should add radios to group', () => {
224
+ registry.add('test-group', radio1);
225
+ registry.add('test-group', radio2);
226
+
227
+ // Test that radios are added by checking if select works
228
+ const spy1 = jest.spyOn(radio1, 'onInput');
229
+ const spy2 = jest.spyOn(radio2, 'onInput');
230
+
231
+ registry.select('test-group', 'option1');
232
+
233
+ expect(spy1).toHaveBeenCalledWith('option1');
234
+ expect(spy2).toHaveBeenCalledWith('option1');
235
+ });
236
+
237
+ it('should remove radios from group', () => {
238
+ registry.add('test-group', radio1);
239
+ registry.add('test-group', radio2);
240
+
241
+ registry.remove('test-group', radio1);
242
+
243
+ // Test that radio1 is removed by checking if select only affects radio2
244
+ const spy1 = jest.spyOn(radio1, 'onInput');
245
+ const spy2 = jest.spyOn(radio2, 'onInput');
246
+
247
+ registry.select('test-group', 'option1');
248
+
249
+ expect(spy1).not.toHaveBeenCalled();
250
+ expect(spy2).toHaveBeenCalledWith('option1');
251
+ });
252
+
253
+ it('should select value in group', () => {
254
+ const spy1 = jest.spyOn(radio1, 'onInput');
255
+ const spy2 = jest.spyOn(radio2, 'onInput');
256
+
257
+ registry.add('test-group', radio1);
258
+ registry.add('test-group', radio2);
259
+
260
+ registry.select('test-group', 'option1');
261
+
262
+ expect(spy1).toHaveBeenCalledWith('option1');
263
+ expect(spy2).toHaveBeenCalledWith('option1');
264
+ });
265
+
266
+ it('should handle non-existent groups gracefully', () => {
267
+ expect(() => {
268
+ registry.remove('non-existent', radio1);
269
+ registry.select('non-existent', 'value');
270
+ }).not.toThrow();
271
+ });
272
+
273
+ it('should handle empty groups gracefully', () => {
274
+ expect(() => {
275
+ registry.select('empty-group', 'value');
276
+ }).not.toThrow();
277
+ });
278
+ });
@@ -0,0 +1,169 @@
1
+ import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
2
+ import { Meta, StoryObj } from '@storybook/angular';
3
+
4
+ import { ZenRadio } from './radio';
5
+
6
+ interface StoryParams {
7
+ selectedColor?: string;
8
+ }
9
+
10
+ type Options = ZenRadio & StoryParams;
11
+
12
+ export default {
13
+ title: 'Components/Radio',
14
+ component: ZenRadio,
15
+ argTypes: {
16
+ value: {
17
+ table: {
18
+ category: 'models',
19
+ type: {
20
+ summary: 'string | null',
21
+ },
22
+ defaultValue: {
23
+ summary: 'null',
24
+ },
25
+ },
26
+ control: 'text',
27
+ },
28
+ name: {
29
+ table: {
30
+ category: 'models',
31
+ type: {
32
+ summary: 'string',
33
+ },
34
+ defaultValue: {
35
+ summary: 'radio',
36
+ },
37
+ },
38
+ control: 'text',
39
+ },
40
+ option: {
41
+ table: {
42
+ 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
+ },
68
+ },
69
+ },
70
+ onInput: {
71
+ table: {
72
+ readonly: true,
73
+ type: {
74
+ summary: '(value: string | null) => void',
75
+ },
76
+ },
77
+ },
78
+ },
79
+ args: {
80
+ value: null,
81
+ name: 'radio-group',
82
+ option: 'option1',
83
+ disabled: false,
84
+ required: false,
85
+ },
86
+ } satisfies Meta<Options>;
87
+
88
+ type Story = StoryObj<Options>;
89
+
90
+ export const Default: Story = {};
91
+
92
+ export const WithLabel: Story = {
93
+ render: () => ({
94
+ template: `
95
+ <div style="display: flex; align-items: center; gap: 0.25rem">
96
+ <zen-radio name="label-example" option="option1" />
97
+ <label for="label-example"> With label </label>
98
+ </div>
99
+ `,
100
+ }),
101
+ };
102
+
103
+ export const NgModel: Story = {
104
+ 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
+ `,
133
+ }),
134
+ };
135
+
136
+ export const AsFromControl: Story = {
137
+ render: () => {
138
+ return {
139
+ moduleMetadata: {
140
+ imports: [ReactiveFormsModule],
141
+ },
142
+ props: {
143
+ colorControl: new FormControl('green'),
144
+ },
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
+ };
168
+ },
169
+ };
@@ -0,0 +1,114 @@
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';
12
+
13
+ import { ZenFormControl, ZenFormControlProvider } from '../form-control';
14
+ import { ZenRadioRegistry } from './radio.registry';
15
+
16
+ /**
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.
21
+ *
22
+ * @example
23
+ * <zen-radio name="group" option="option1" [(ngModel)]="selectedValue" /> Option 1
24
+ * <zen-radio name="group" option="option2" [(ngModel)]="selectedValue" /> Option 2
25
+ *
26
+ * ### CSS Custom Properties
27
+ *
28
+ * You can customize the component using CSS custom properties:
29
+ *
30
+ * ```css
31
+ * :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%);
37
+ * }
38
+ * ```
39
+ *
40
+ * @implements {ZenFormControl<string | null>}
41
+ *
42
+ * @author Konrad StΔ™pieΕ„
43
+ * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
44
+ * @see [GitHub](https://github.com/kstepien3/ng-zen)
45
+ */
46
+ @Component({
47
+ selector: 'zen-radio',
48
+ template: `
49
+ <input
50
+ [attr.aria-checked]="checked()"
51
+ [attr.aria-disabled]="disabled()"
52
+ [checked]="checked()"
53
+ [disabled]="disabled()"
54
+ [name]="name()"
55
+ [value]="option()"
56
+ (change)="onRadioChange()"
57
+ #inputElement
58
+ type="radio"
59
+ />
60
+ @if (checked()) {
61
+ <span class="radio-dot"></span>
62
+ }
63
+ `,
64
+ styleUrls: ['./radio.scss'],
65
+ changeDetection: ChangeDetectionStrategy.OnPush,
66
+ imports: [FormsModule],
67
+ providers: [ZenFormControlProvider(ZenRadio)],
68
+ host: {
69
+ '(blur)': 'onTouched()',
70
+ },
71
+ })
72
+ export class ZenRadio extends ZenFormControl<string | null> {
73
+ /**
74
+ * The value of the radio button group.
75
+ * This should be bound to the same model for all radio buttons in a group.
76
+ */
77
+ readonly value = model<string | null>(null);
78
+
79
+ /**
80
+ * The name attribute for the radio button group.
81
+ * Radio buttons with the same name will be grouped together.
82
+ */
83
+ readonly name = input.required<string>();
84
+
85
+ /**
86
+ * The value for this specific radio button.
87
+ */
88
+ readonly option = input.required<string>();
89
+
90
+ /**
91
+ * Determines if this radio button is checked based on the group value.
92
+ */
93
+ protected checked = computed<boolean>(() => this.value() === this.option());
94
+ /**
95
+ * Handles radio button selection using native change event.
96
+ */
97
+
98
+ private readonly registry = inject(ZenRadioRegistry);
99
+ private readonly destroyRef = inject(DestroyRef);
100
+
101
+ constructor() {
102
+ super();
103
+ afterNextRender(() => {
104
+ this.registry.add(this.name(), this);
105
+ });
106
+
107
+ this.destroyRef.onDestroy(() => this.registry.remove(this.name(), this));
108
+ }
109
+
110
+ protected onRadioChange(): void {
111
+ if (this.disabled()) return;
112
+ this.registry.select(this.name(), this.option());
113
+ }
114
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../../../src/schematics/components/schema.ts"],"names":[],"mappings":"","sourcesContent":["import { GeneratorSchemaBase } from '../../types';\n\nexport type ComponentType =\n | 'avatar'\n | 'button'\n | 'checkbox'\n | 'divider'\n | 'form-control'\n | 'icon'\n | 'input'\n | 'skeleton'\n | 'switch'\n | 'textarea'\n | 'alert';\n\nexport interface Schema extends GeneratorSchemaBase {\n components: ComponentType[];\n}\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../../../src/schematics/components/schema.ts"],"names":[],"mappings":"","sourcesContent":["import { GeneratorSchemaBase } from '../../types';\n\nexport type ComponentType =\n | 'alert'\n | 'avatar'\n | 'button'\n | 'checkbox'\n | 'divider'\n | 'form-control'\n | 'icon'\n | 'input'\n | 'radio'\n | 'skeleton'\n | 'switch'\n | 'textarea';\n\nexport interface Schema extends GeneratorSchemaBase {\n components: ComponentType[];\n}\n"]}
@@ -29,6 +29,7 @@
29
29
  "items": {
30
30
  "type": "string",
31
31
  "enum": [
32
+ "alert",
32
33
  "avatar",
33
34
  "button",
34
35
  "checkbox",
@@ -36,10 +37,10 @@
36
37
  "form-control",
37
38
  "icon",
38
39
  "input",
40
+ "radio",
39
41
  "skeleton",
40
42
  "switch",
41
- "textarea",
42
- "alert"
43
+ "textarea"
43
44
  ]
44
45
  },
45
46
  "multiselect": true,
@@ -1,6 +1,7 @@
1
1
  import { GeneratorSchemaBase } from '../../types';
2
2
 
3
3
  export type ComponentType =
4
+ | 'alert'
4
5
  | 'avatar'
5
6
  | 'button'
6
7
  | 'checkbox'
@@ -8,10 +9,10 @@ export type ComponentType =
8
9
  | 'form-control'
9
10
  | 'icon'
10
11
  | 'input'
12
+ | 'radio'
11
13
  | 'skeleton'
12
14
  | 'switch'
13
- | 'textarea'
14
- | 'alert';
15
+ | 'textarea';
15
16
 
16
17
  export interface Schema extends GeneratorSchemaBase {
17
18
  components: ComponentType[];