ngx-opalbytes-components 1.25.0 → 1.26.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-opalbytes-components",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.0.0",
6
6
  "@angular/core": "^21.0.0",
@@ -27,4 +27,7 @@ export * from "./error/error"
27
27
 
28
28
  export * from "./card/card.component"
29
29
  export * from "./base-toggle/base-toggle.component"
30
+ export * from "./slide-toggle/slide-toggle.component"
31
+ export * from "./radio/radio-group.component"
32
+ export * from "./radio/radio-button.component"
30
33
  export * from "./footer/footer"
@@ -0,0 +1,95 @@
1
+ :host {
2
+ display: inline-flex;
3
+ }
4
+
5
+ .cao-radio {
6
+ display: inline-flex;
7
+ align-items: center;
8
+ gap: 8px;
9
+ cursor: pointer;
10
+ user-select: none;
11
+ }
12
+
13
+ .cao-radio--disabled {
14
+ cursor: not-allowed;
15
+ }
16
+
17
+ .cao-radio__input {
18
+ position: absolute;
19
+ width: 1px;
20
+ height: 1px;
21
+ margin: -1px;
22
+ padding: 0;
23
+ border: 0;
24
+ overflow: hidden;
25
+ clip: rect(0 0 0 0);
26
+ clip-path: inset(50%);
27
+ white-space: nowrap;
28
+ }
29
+
30
+ .cao-radio__control {
31
+ display: inline-flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ flex: none;
35
+ width: var(--cao-radio-size, 20px);
36
+ height: var(--cao-radio-size, 20px);
37
+ box-sizing: border-box;
38
+ border: 2px solid var(--cao-radio-unselected, #6b7280);
39
+ border-radius: 50%;
40
+ transition: border-color var(--cao-radio-duration, 200ms) ease;
41
+ }
42
+
43
+ .cao-radio__dot {
44
+ width: 50%;
45
+ height: 50%;
46
+ border-radius: 50%;
47
+ background-color: var(--cao-radio-color, #4f46e5);
48
+ transform: scale(0);
49
+ transition:
50
+ transform var(--cao-radio-duration, 200ms) ease,
51
+ background-color var(--cao-radio-duration, 200ms) ease;
52
+ }
53
+
54
+ .cao-radio__label {
55
+ color: var(--cao-radio-label, #111827);
56
+ font-size: 0.875rem;
57
+ line-height: 1.25rem;
58
+ }
59
+
60
+ .cao-radio__label:empty {
61
+ display: none;
62
+ }
63
+
64
+ .cao-radio:not(.cao-radio--disabled):hover .cao-radio__control,
65
+ .cao-radio--checked .cao-radio__control {
66
+ border-color: var(--cao-radio-color, #4f46e5);
67
+ }
68
+
69
+ .cao-radio--checked .cao-radio__dot {
70
+ transform: scale(1);
71
+ }
72
+
73
+ .cao-radio--disabled .cao-radio__control {
74
+ border-color: var(--cao-radio-disabled, #d1d5db);
75
+ }
76
+
77
+ .cao-radio--disabled .cao-radio__dot {
78
+ background-color: var(--cao-radio-disabled, #d1d5db);
79
+ }
80
+
81
+ .cao-radio--disabled .cao-radio__label {
82
+ color: var(--cao-radio-label-disabled, #9ca3af);
83
+ }
84
+
85
+ .cao-radio__input:focus-visible + .cao-radio__control {
86
+ box-shadow: 0 0 0 3px var(--cao-radio-focus-ring, rgba(79, 70, 229, 0.35));
87
+ }
88
+
89
+ .cao-radio--label-before .cao-radio__control {
90
+ order: 2;
91
+ }
92
+
93
+ .cao-radio--label-before .cao-radio__label {
94
+ order: 1;
95
+ }
@@ -0,0 +1,32 @@
1
+ <label
2
+ class="cao-radio"
3
+ [class.cao-radio--checked]="isSelected"
4
+ [class.cao-radio--disabled]="effectiveDisabled"
5
+ [class.cao-radio--label-before]="effectiveLabelPosition === 'before'">
6
+ <input
7
+ #input
8
+ class="cao-radio__input"
9
+ type="radio"
10
+ [checked]="isSelected"
11
+ [disabled]="effectiveDisabled"
12
+ [required]="isRequired"
13
+ [attr.name]="effectiveName"
14
+ [attr.id]="inputId"
15
+ [attr.aria-label]="ariaLabel || null"
16
+ [attr.aria-labelledby]="ariaLabelledBy || null"
17
+ [attr.data-cy]="dataCy || null"
18
+ (change)="select()"
19
+ (blur)="onBlur()" />
20
+
21
+ <span class="cao-radio__control" aria-hidden="true">
22
+ <span class="cao-radio__dot"></span>
23
+ </span>
24
+
25
+ <span class="cao-radio__label">
26
+ @if (label) {
27
+ {{ label }}
28
+ } @else {
29
+ <ng-content />
30
+ }
31
+ </span>
32
+ </label>
@@ -0,0 +1,133 @@
1
+ import { CommonModule } from "@angular/common";
2
+ import {
3
+ Component,
4
+ forwardRef,
5
+ HostBinding,
6
+ Inject,
7
+ Input,
8
+ Optional,
9
+ } from "@angular/core";
10
+
11
+ import { CaoRadioGroupComponent } from "./radio-group.component";
12
+
13
+ let nextRadioId = 0;
14
+
15
+ @Component({
16
+ selector: "cao-radio-button",
17
+ standalone: true,
18
+ imports: [CommonModule],
19
+ templateUrl: "./radio-button.component.html",
20
+ styleUrl: "./radio-button.component.css",
21
+ })
22
+ export class CaoRadioButtonComponent {
23
+ @Input() value: unknown = undefined;
24
+ @Input() label = "";
25
+ @Input() isDisabled = false;
26
+ @Input() labelPosition?: "before" | "after";
27
+ @Input() id = "";
28
+ @Input() name = "";
29
+ @Input() ariaLabel = "";
30
+ @Input() ariaLabelledBy = "";
31
+ @Input() dataCy = "";
32
+
33
+ @Input() color?: string;
34
+ @Input() unselectedColor?: string;
35
+ @Input() disabledColor?: string;
36
+ @Input() labelColor?: string;
37
+ @Input() disabledLabelColor?: string;
38
+ @Input() focusRingColor?: string;
39
+ @Input() size?: string;
40
+ @Input() animationDuration?: string;
41
+
42
+ @Input() set disabled(value: boolean) {
43
+ this.isDisabled = value;
44
+ }
45
+
46
+ get disabled(): boolean {
47
+ return this.isDisabled;
48
+ }
49
+
50
+ private readonly uid = `cao-radio-${nextRadioId++}`;
51
+
52
+ constructor(
53
+ @Optional()
54
+ @Inject(forwardRef(() => CaoRadioGroupComponent))
55
+ readonly group: CaoRadioGroupComponent | null,
56
+ ) {}
57
+
58
+ get effectiveDisabled(): boolean {
59
+ return this.isDisabled || !!this.group?.isEffectivelyDisabled;
60
+ }
61
+
62
+ get effectiveLabelPosition(): "before" | "after" {
63
+ return this.labelPosition ?? this.group?.labelPosition ?? "after";
64
+ }
65
+
66
+ get effectiveName(): string {
67
+ return this.name || this.group?.groupName || this.uid;
68
+ }
69
+
70
+ get inputId(): string {
71
+ return this.id || this.uid;
72
+ }
73
+
74
+ get isSelected(): boolean {
75
+ return this.group?.isValueSelected(this.value) ?? false;
76
+ }
77
+
78
+ get isRequired(): boolean {
79
+ return this.group?.required ?? false;
80
+ }
81
+
82
+ @HostBinding("style")
83
+ get hostStyles(): Record<string, string> {
84
+ const styles: Record<string, string> = {};
85
+
86
+ if (this.color) {
87
+ styles["--cao-radio-color"] = this.color;
88
+ }
89
+ if (this.unselectedColor) {
90
+ styles["--cao-radio-unselected"] = this.unselectedColor;
91
+ }
92
+ if (this.disabledColor) {
93
+ styles["--cao-radio-disabled"] = this.disabledColor;
94
+ }
95
+ if (this.labelColor) {
96
+ styles["--cao-radio-label"] = this.labelColor;
97
+ }
98
+ if (this.disabledLabelColor) {
99
+ styles["--cao-radio-label-disabled"] = this.disabledLabelColor;
100
+ }
101
+ if (this.focusRingColor) {
102
+ styles["--cao-radio-focus-ring"] = this.focusRingColor;
103
+ }
104
+ if (this.size) {
105
+ styles["--cao-radio-size"] = this.size;
106
+ }
107
+ if (this.animationDuration) {
108
+ styles["--cao-radio-duration"] = this.animationDuration;
109
+ }
110
+
111
+ return styles;
112
+ }
113
+
114
+ select(): void {
115
+ if (this.effectiveDisabled) {
116
+ return;
117
+ }
118
+
119
+ this.group?.select(this);
120
+ }
121
+
122
+ onBlur(): void {
123
+ this.group?.markAsTouched();
124
+ }
125
+
126
+ enable(): void {
127
+ this.isDisabled = false;
128
+ }
129
+
130
+ disable(): void {
131
+ this.isDisabled = true;
132
+ }
133
+ }
@@ -0,0 +1,94 @@
1
+ import { FormsModule } from '@angular/forms';
2
+
3
+ import type { Meta, StoryObj } from '@storybook/angular-vite';
4
+
5
+ import { CaoRadioButtonComponent } from './radio-button.component';
6
+ import { CaoRadioGroupComponent } from './radio-group.component';
7
+
8
+ const meta: Meta<CaoRadioButtonComponent> = {
9
+ title: 'Componentes/Radio Button',
10
+ component: CaoRadioButtonComponent,
11
+ tags: ['autodocs'],
12
+ decorators: [
13
+ (story) => ({
14
+ ...story,
15
+ moduleMetadata: {
16
+ imports: [FormsModule, CaoRadioGroupComponent, CaoRadioButtonComponent],
17
+ },
18
+ }),
19
+ ],
20
+ argTypes: {
21
+ labelPosition: { control: 'select', options: ['before', 'after'] },
22
+ isDisabled: { control: 'boolean' },
23
+ disabled: { control: 'boolean' },
24
+ color: { control: 'color' },
25
+ unselectedColor: { control: 'color' },
26
+ disabledColor: { control: 'color' },
27
+ labelColor: { control: 'color' },
28
+ disabledLabelColor: { control: 'color' },
29
+ focusRingColor: { control: 'color' },
30
+ size: { control: 'text' },
31
+ animationDuration: { control: 'text' },
32
+ },
33
+ args: {
34
+ value: 'option-1',
35
+ label: 'Opção selecionada',
36
+ labelPosition: 'after',
37
+ isDisabled: false,
38
+ },
39
+ render: (args) => ({
40
+ props: {
41
+ ...args,
42
+ selectedValue: 'option-1',
43
+ },
44
+ template: `
45
+ <cao-radio-group
46
+ [(ngModel)]="selectedValue"
47
+ [labelPosition]="labelPosition">
48
+ <cao-radio-button
49
+ value="option-1"
50
+ [label]="label"
51
+ [isDisabled]="isDisabled"
52
+ [labelPosition]="labelPosition"
53
+ [color]="color"
54
+ [unselectedColor]="unselectedColor"
55
+ [disabledColor]="disabledColor"
56
+ [labelColor]="labelColor"
57
+ [disabledLabelColor]="disabledLabelColor"
58
+ [focusRingColor]="focusRingColor"
59
+ [size]="size"
60
+ [animationDuration]="animationDuration" />
61
+ <cao-radio-button value="option-2" label="Outra opção" />
62
+ </cao-radio-group>
63
+ <p>Selecionado: {{ selectedValue }}</p>
64
+ `,
65
+ }),
66
+ };
67
+
68
+ export default meta;
69
+ type Story = StoryObj<CaoRadioButtonComponent>;
70
+
71
+ export const Padrao: Story = {};
72
+
73
+ export const Desabilitado: Story = {
74
+ args: {
75
+ isDisabled: true,
76
+ label: 'Opção desabilitada',
77
+ },
78
+ };
79
+
80
+ export const CoresCustomizadas: Story = {
81
+ args: {
82
+ color: '#16a34a',
83
+ unselectedColor: '#94a3b8',
84
+ labelColor: '#166534',
85
+ size: '24px',
86
+ },
87
+ };
88
+
89
+ export const LabelAntes: Story = {
90
+ args: {
91
+ labelPosition: 'before',
92
+ label: 'Label antes',
93
+ },
94
+ };
@@ -0,0 +1,21 @@
1
+ :host {
2
+ --cao-radio-color: #4f46e5;
3
+ --cao-radio-unselected: #6b7280;
4
+ --cao-radio-disabled: #d1d5db;
5
+ --cao-radio-label: #111827;
6
+ --cao-radio-label-disabled: #9ca3af;
7
+ --cao-radio-focus-ring: rgba(79, 70, 229, 0.35);
8
+ --cao-radio-size: 20px;
9
+ --cao-radio-duration: 200ms;
10
+ --cao-radio-gap: 8px;
11
+
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--cao-radio-gap);
15
+ }
16
+
17
+ :host(.cao-radio-group--horizontal) {
18
+ flex-direction: row;
19
+ flex-wrap: wrap;
20
+ align-items: center;
21
+ }
@@ -0,0 +1,216 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ ChangeDetectorRef,
4
+ Component,
5
+ EventEmitter,
6
+ forwardRef,
7
+ HostBinding,
8
+ inject,
9
+ Input,
10
+ Output,
11
+ } from "@angular/core";
12
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
13
+
14
+ import type { CaoRadioButtonComponent } from "./radio-button.component";
15
+
16
+ let nextRadioGroupId = 0;
17
+
18
+ export interface ICaoRadioChangeEvent {
19
+ value: unknown;
20
+ source: CaoRadioButtonComponent;
21
+ }
22
+
23
+ @Component({
24
+ selector: "cao-radio-group",
25
+ standalone: true,
26
+ template: "<ng-content />",
27
+ styleUrl: "./radio-group.component.css",
28
+ providers: [
29
+ {
30
+ provide: NG_VALUE_ACCESSOR,
31
+ useExisting: forwardRef(() => CaoRadioGroupComponent),
32
+ multi: true,
33
+ },
34
+ ],
35
+ changeDetection: ChangeDetectionStrategy.OnPush,
36
+ })
37
+ export class CaoRadioGroupComponent implements ControlValueAccessor {
38
+ @Input() value: unknown = undefined;
39
+ @Input() name = "";
40
+ @Input() isDisabled = false;
41
+ @Input() required = false;
42
+ @Input() orientation: "vertical" | "horizontal" = "vertical";
43
+ @Input() labelPosition: "before" | "after" = "after";
44
+ @Input() compareWith: (option: unknown, value: unknown) => boolean = (
45
+ option,
46
+ value,
47
+ ) => option === value;
48
+
49
+ @Input() color?: string;
50
+ @Input() unselectedColor?: string;
51
+ @Input() disabledColor?: string;
52
+ @Input() labelColor?: string;
53
+ @Input() disabledLabelColor?: string;
54
+ @Input() focusRingColor?: string;
55
+ @Input() size?: string;
56
+ @Input() animationDuration?: string;
57
+ @Input() gap?: string;
58
+
59
+ @Input() ariaLabel = "";
60
+ @Input() ariaLabelledBy = "";
61
+ @Input() dataCy = "";
62
+
63
+ @Input() set disabled(value: boolean) {
64
+ this.isDisabled = value;
65
+ this.cdr.markForCheck();
66
+ }
67
+
68
+ get disabled(): boolean {
69
+ return this.isDisabled;
70
+ }
71
+
72
+ @Output() readonly valueChange = new EventEmitter<unknown>();
73
+ @Output() readonly selectionChange = new EventEmitter<ICaoRadioChangeEvent>();
74
+
75
+ readonly resolvedName = `cao-radio-group-${nextRadioGroupId++}`;
76
+
77
+ private readonly cdr = inject(ChangeDetectorRef);
78
+ private formDisabled = false;
79
+ private onChange: (value: unknown) => void = () => {};
80
+ private onTouched: () => void = () => {};
81
+
82
+ get isEffectivelyDisabled(): boolean {
83
+ return this.isDisabled || this.formDisabled;
84
+ }
85
+
86
+ @HostBinding("attr.role")
87
+ readonly role = "radiogroup";
88
+
89
+ @HostBinding("class.cao-radio-group--horizontal")
90
+ get isHorizontal(): boolean {
91
+ return this.orientation === "horizontal";
92
+ }
93
+
94
+ @HostBinding("class.cao-radio-group--disabled")
95
+ get isDisabledClass(): boolean {
96
+ return this.isEffectivelyDisabled;
97
+ }
98
+
99
+ @HostBinding("attr.aria-label")
100
+ get ariaLabelAttr(): string | null {
101
+ return this.ariaLabel || null;
102
+ }
103
+
104
+ @HostBinding("attr.aria-labelledby")
105
+ get ariaLabelledByAttr(): string | null {
106
+ return this.ariaLabelledBy || null;
107
+ }
108
+
109
+ @HostBinding("attr.aria-disabled")
110
+ get ariaDisabledAttr(): boolean | null {
111
+ return this.isEffectivelyDisabled || null;
112
+ }
113
+
114
+ @HostBinding("attr.aria-required")
115
+ get ariaRequiredAttr(): boolean | null {
116
+ return this.required || null;
117
+ }
118
+
119
+ @HostBinding("attr.data-cy")
120
+ get dataCyAttr(): string | null {
121
+ return this.dataCy || null;
122
+ }
123
+
124
+ @HostBinding("style")
125
+ get hostStyles(): Record<string, string> {
126
+ const styles: Record<string, string> = {};
127
+
128
+ if (this.color) {
129
+ styles["--cao-radio-color"] = this.color;
130
+ }
131
+ if (this.unselectedColor) {
132
+ styles["--cao-radio-unselected"] = this.unselectedColor;
133
+ }
134
+ if (this.disabledColor) {
135
+ styles["--cao-radio-disabled"] = this.disabledColor;
136
+ }
137
+ if (this.labelColor) {
138
+ styles["--cao-radio-label"] = this.labelColor;
139
+ }
140
+ if (this.disabledLabelColor) {
141
+ styles["--cao-radio-label-disabled"] = this.disabledLabelColor;
142
+ }
143
+ if (this.focusRingColor) {
144
+ styles["--cao-radio-focus-ring"] = this.focusRingColor;
145
+ }
146
+ if (this.size) {
147
+ styles["--cao-radio-size"] = this.size;
148
+ }
149
+ if (this.animationDuration) {
150
+ styles["--cao-radio-duration"] = this.animationDuration;
151
+ }
152
+ if (this.gap) {
153
+ styles["--cao-radio-gap"] = this.gap;
154
+ }
155
+
156
+ return styles;
157
+ }
158
+
159
+ get groupName(): string {
160
+ return this.name || this.resolvedName;
161
+ }
162
+
163
+ isValueSelected(value: unknown): boolean {
164
+ return this.compareWith(this.value, value);
165
+ }
166
+
167
+ select(button: CaoRadioButtonComponent): void {
168
+ if (this.isEffectivelyDisabled || button.effectiveDisabled) {
169
+ return;
170
+ }
171
+
172
+ if (this.compareWith(this.value, button.value)) {
173
+ this.markAsTouched();
174
+ return;
175
+ }
176
+
177
+ this.value = button.value;
178
+ this.onChange(this.value);
179
+ this.valueChange.emit(this.value);
180
+ this.selectionChange.emit({ value: this.value, source: button });
181
+ this.markAsTouched();
182
+ this.cdr.markForCheck();
183
+ }
184
+
185
+ markAsTouched(): void {
186
+ this.onTouched();
187
+ }
188
+
189
+ enable(): void {
190
+ this.isDisabled = false;
191
+ this.cdr.markForCheck();
192
+ }
193
+
194
+ disable(): void {
195
+ this.isDisabled = true;
196
+ this.cdr.markForCheck();
197
+ }
198
+
199
+ writeValue(value: unknown): void {
200
+ this.value = value;
201
+ this.cdr.markForCheck();
202
+ }
203
+
204
+ registerOnChange(fn: (value: unknown) => void): void {
205
+ this.onChange = fn;
206
+ }
207
+
208
+ registerOnTouched(fn: () => void): void {
209
+ this.onTouched = fn;
210
+ }
211
+
212
+ setDisabledState(isDisabled: boolean): void {
213
+ this.formDisabled = isDisabled;
214
+ this.cdr.markForCheck();
215
+ }
216
+ }