ngx-opalbytes-components 1.21.0 → 1.23.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/README.md +173 -0
- package/fesm2022/ngx-opalbytes-components.mjs +464 -20
- package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/shared/components/error/error.css +10 -0
- package/src/lib/shared/components/error/error.html +5 -0
- package/src/lib/shared/components/error/error.spec.ts +76 -0
- package/src/lib/shared/components/error/error.stories.ts +61 -0
- package/src/lib/shared/components/error/error.ts +33 -0
- package/src/lib/shared/components/form-field/error-state-matcher.ts +41 -0
- package/src/lib/shared/components/form-field/form-field-control.ts +10 -0
- package/src/lib/shared/components/form-field/form-field.css +154 -0
- package/src/lib/shared/components/form-field/form-field.html +51 -0
- package/src/lib/shared/components/form-field/form-field.spec.ts +114 -0
- package/src/lib/shared/components/form-field/form-field.stories.ts +177 -0
- package/src/lib/shared/components/form-field/form-field.ts +118 -0
- package/src/lib/shared/components/hint/hint.css +10 -0
- package/src/lib/shared/components/hint/hint.html +1 -0
- package/src/lib/shared/components/hint/hint.spec.ts +22 -0
- package/src/lib/shared/components/hint/hint.stories.ts +53 -0
- package/src/lib/shared/components/hint/hint.ts +9 -0
- package/src/lib/shared/components/index.ts +12 -1
- package/src/lib/shared/components/skeleton/skeleton.css +92 -0
- package/src/lib/shared/components/skeleton/skeleton.html +3 -0
- package/src/lib/shared/components/skeleton/skeleton.spec.ts +22 -0
- package/src/lib/shared/components/skeleton/skeleton.stories.ts +131 -0
- package/src/lib/shared/components/skeleton/skeleton.ts +46 -0
- package/types/ngx-opalbytes-components.d.ts +178 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/angular-vite';
|
|
4
|
+
|
|
5
|
+
import { CaoFormField } from './form-field';
|
|
6
|
+
import { CaoBaseInput } from '../base-input/base-input';
|
|
7
|
+
import { CaoError } from '../error/error';
|
|
8
|
+
import { CaoHint } from '../hint/hint';
|
|
9
|
+
|
|
10
|
+
const storyImports = [ReactiveFormsModule, CaoFormField, CaoBaseInput, CaoHint, CaoError];
|
|
11
|
+
|
|
12
|
+
const renderFormField = (inner: string, props: Record<string, unknown> = {}) => ({
|
|
13
|
+
template: `<div style="max-width: 420px;">${inner}</div>`,
|
|
14
|
+
moduleMetadata: { imports: storyImports },
|
|
15
|
+
props,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const control = () => new FormControl('', [Validators.required]);
|
|
19
|
+
|
|
20
|
+
const meta: Meta<CaoFormField> = {
|
|
21
|
+
title: 'Componentes/Form Field',
|
|
22
|
+
component: CaoFormField,
|
|
23
|
+
subcomponents: {
|
|
24
|
+
caoBaseInput: CaoBaseInput,
|
|
25
|
+
caoHint: CaoHint,
|
|
26
|
+
caoError: CaoError,
|
|
27
|
+
},
|
|
28
|
+
tags: ['autodocs'],
|
|
29
|
+
argTypes: {
|
|
30
|
+
label: { control: 'text' },
|
|
31
|
+
labelInside: { control: 'boolean' },
|
|
32
|
+
hintLabel: { control: 'text' },
|
|
33
|
+
leadingIcon: { control: 'text' },
|
|
34
|
+
trailingIcon: { control: 'text' },
|
|
35
|
+
inputBackground: { control: 'color' },
|
|
36
|
+
borderColor: { control: 'color' },
|
|
37
|
+
borderWidth: { control: 'text' },
|
|
38
|
+
borderRadius: { control: 'text' },
|
|
39
|
+
errorColor: { control: 'color' },
|
|
40
|
+
labelColor: { control: 'color' },
|
|
41
|
+
hintColor: { control: 'color' },
|
|
42
|
+
textColor: { control: 'color' },
|
|
43
|
+
focusBorderColor: { control: 'color' },
|
|
44
|
+
successBorderColor: { control: 'color' },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<CaoFormField>;
|
|
50
|
+
|
|
51
|
+
export const Padrao: Story = {
|
|
52
|
+
render: () =>
|
|
53
|
+
renderFormField(
|
|
54
|
+
`
|
|
55
|
+
<cao-form-field forId="padrao" [label]="'Nome'" [control]="control">
|
|
56
|
+
<cao-input [control]="control" [placeholder]="'Digite seu nome'"></cao-input>
|
|
57
|
+
</cao-form-field>
|
|
58
|
+
`,
|
|
59
|
+
{ control: control() }
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const LabelDentro: Story = {
|
|
64
|
+
render: () =>
|
|
65
|
+
renderFormField(
|
|
66
|
+
`
|
|
67
|
+
<cao-form-field forId="label-dentro" [label]="'Nome'" [labelInside]="true" [control]="control">
|
|
68
|
+
<cao-input [control]="control" [placeholder]="' '"></cao-input>
|
|
69
|
+
</cao-form-field>
|
|
70
|
+
`,
|
|
71
|
+
{ control: control() }
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const ComHint: Story = {
|
|
76
|
+
render: () =>
|
|
77
|
+
renderFormField(
|
|
78
|
+
`
|
|
79
|
+
<cao-form-field forId="cep" [label]="'CEP'" [control]="control">
|
|
80
|
+
<cao-input [control]="control" [placeholder]="'00000-000'"></cao-input>
|
|
81
|
+
<cao-hint>Formato: 00000-000</cao-hint>
|
|
82
|
+
</cao-form-field>
|
|
83
|
+
`,
|
|
84
|
+
{ control: control() }
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const ComErro: Story = {
|
|
89
|
+
render: () => {
|
|
90
|
+
const erroControl = control();
|
|
91
|
+
erroControl.markAsTouched();
|
|
92
|
+
return renderFormField(
|
|
93
|
+
`
|
|
94
|
+
<cao-form-field forId="email" [label]="'E-mail'" [control]="control">
|
|
95
|
+
<cao-input [control]="control" [placeholder]="'Digite seu e-mail'"></cao-input>
|
|
96
|
+
<cao-error>Campo obrigatório</cao-error>
|
|
97
|
+
</cao-form-field>
|
|
98
|
+
`,
|
|
99
|
+
{ control: erroControl }
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const ComIcones: Story = {
|
|
105
|
+
render: () =>
|
|
106
|
+
renderFormField(
|
|
107
|
+
`
|
|
108
|
+
<cao-form-field
|
|
109
|
+
forId="buscar"
|
|
110
|
+
[label]="'Buscar'"
|
|
111
|
+
leadingIcon="Search"
|
|
112
|
+
trailingIcon="SlidersHorizontal"
|
|
113
|
+
iconColor="#4f46e5"
|
|
114
|
+
[control]="control">
|
|
115
|
+
<cao-input [control]="control" [placeholder]="'Pesquisar...'"></cao-input>
|
|
116
|
+
</cao-form-field>
|
|
117
|
+
`,
|
|
118
|
+
{ control: control() }
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const ComMascaraCPF: Story = {
|
|
123
|
+
render: () =>
|
|
124
|
+
renderFormField(
|
|
125
|
+
`
|
|
126
|
+
<cao-form-field forId="cpf" [label]="'CPF'" [control]="control">
|
|
127
|
+
<cao-input
|
|
128
|
+
[control]="control"
|
|
129
|
+
[placeholder]="'000.000.000-00'"
|
|
130
|
+
[mask]="'000.000.000-00'"></cao-input>
|
|
131
|
+
</cao-form-field>
|
|
132
|
+
`,
|
|
133
|
+
{ control: control() }
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const Desabilitado: Story = {
|
|
138
|
+
render: () =>
|
|
139
|
+
renderFormField(
|
|
140
|
+
`
|
|
141
|
+
<cao-form-field forId="bloqueado" [label]="'Campo bloqueado'" [control]="control">
|
|
142
|
+
<cao-input [control]="control" [isDisabled]="true" [placeholder]="'Valor indisponível'"></cao-input>
|
|
143
|
+
</cao-form-field>
|
|
144
|
+
`,
|
|
145
|
+
{ control: new FormControl({ value: '', disabled: true }) }
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const CoresCustomizadas: Story = {
|
|
150
|
+
render: () => {
|
|
151
|
+
const coresControl = control();
|
|
152
|
+
coresControl.markAsTouched();
|
|
153
|
+
return renderFormField(
|
|
154
|
+
`
|
|
155
|
+
<cao-form-field
|
|
156
|
+
forId="cores"
|
|
157
|
+
[label]="'Personalizado'"
|
|
158
|
+
inputBackground="#f8fafc"
|
|
159
|
+
borderColor="#94a3b8"
|
|
160
|
+
borderWidth="2px"
|
|
161
|
+
borderRadius="16px"
|
|
162
|
+
errorColor="#e11d48"
|
|
163
|
+
labelColor="#0f766e"
|
|
164
|
+
hintColor="#64748b"
|
|
165
|
+
textColor="#0f172a"
|
|
166
|
+
focusBorderColor="#0d9488"
|
|
167
|
+
successBorderColor="#16a34a"
|
|
168
|
+
[control]="control">
|
|
169
|
+
<cao-input [control]="control" [placeholder]="'Cores via input'"></cao-input>
|
|
170
|
+
<cao-hint>Hint customizado</cao-hint>
|
|
171
|
+
<cao-error>Erro customizado</cao-error>
|
|
172
|
+
</cao-form-field>
|
|
173
|
+
`,
|
|
174
|
+
{ control: coresControl }
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Component, computed, input, Input } from '@angular/core';
|
|
2
|
+
import { AbstractControl, Validators } from '@angular/forms';
|
|
3
|
+
|
|
4
|
+
import { IErrorStateMatcher } from './error-state-matcher';
|
|
5
|
+
import { IFormFieldControl } from './form-field-control';
|
|
6
|
+
import { CaoBaseIcon } from '../base-icon/base-icon';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'cao-form-field',
|
|
10
|
+
imports: [CaoBaseIcon],
|
|
11
|
+
templateUrl: './form-field.html',
|
|
12
|
+
styleUrl: './form-field.css',
|
|
13
|
+
})
|
|
14
|
+
export class CaoFormField extends CaoBaseIcon implements IFormFieldControl {
|
|
15
|
+
inputBackground = input<string>();
|
|
16
|
+
borderColor = input<string>();
|
|
17
|
+
borderWidth = input<string>();
|
|
18
|
+
borderRadius = input<string>();
|
|
19
|
+
errorColor = input<string>();
|
|
20
|
+
labelColor = input<string>();
|
|
21
|
+
hintColor = input<string>();
|
|
22
|
+
textColor = input<string>();
|
|
23
|
+
focusBorderColor = input<string>();
|
|
24
|
+
successBorderColor = input<string>();
|
|
25
|
+
|
|
26
|
+
formFieldStyles = computed(() => {
|
|
27
|
+
const styles: Record<string, string> = {};
|
|
28
|
+
|
|
29
|
+
if (this.inputBackground()) {
|
|
30
|
+
styles['--cao-form-field-bg-background'] = this.inputBackground()!;
|
|
31
|
+
}
|
|
32
|
+
if (this.borderColor()) {
|
|
33
|
+
styles['--cao-form-field-border-color'] = this.borderColor()!;
|
|
34
|
+
}
|
|
35
|
+
if (this.borderWidth()) {
|
|
36
|
+
styles['--cao-form-field-border-width'] = this.borderWidth()!;
|
|
37
|
+
}
|
|
38
|
+
if (this.borderRadius()) {
|
|
39
|
+
styles['--cao-form-field-border-radius'] = this.borderRadius()!;
|
|
40
|
+
}
|
|
41
|
+
if (this.errorColor()) {
|
|
42
|
+
styles['--cao-form-field-color-error'] = this.errorColor()!;
|
|
43
|
+
}
|
|
44
|
+
if (this.labelColor()) {
|
|
45
|
+
styles['--cao-form-field-color-label'] = this.labelColor()!;
|
|
46
|
+
}
|
|
47
|
+
if (this.hintColor()) {
|
|
48
|
+
styles['--cao-form-field-color-hint'] = this.hintColor()!;
|
|
49
|
+
}
|
|
50
|
+
if (this.textColor()) {
|
|
51
|
+
styles['--cao-form-field-color-text'] = this.textColor()!;
|
|
52
|
+
}
|
|
53
|
+
if (this.focusBorderColor()) {
|
|
54
|
+
styles['--cao-form-field-focus-border'] = this.focusBorderColor()!;
|
|
55
|
+
}
|
|
56
|
+
if (this.successBorderColor()) {
|
|
57
|
+
styles['--cao-form-field-success-border'] = this.successBorderColor()!;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return styles;
|
|
61
|
+
});
|
|
62
|
+
@Input() label = '';
|
|
63
|
+
@Input() labelInside = false;
|
|
64
|
+
@Input() hintLabel = '';
|
|
65
|
+
@Input() control: AbstractControl | null = null;
|
|
66
|
+
@Input() forId = '';
|
|
67
|
+
@Input() errorStateMatcher?: IErrorStateMatcher;
|
|
68
|
+
@Input() leadingIcon?: string;
|
|
69
|
+
@Input() trailingIcon?: string;
|
|
70
|
+
|
|
71
|
+
_isSubmitted = false;
|
|
72
|
+
_focused = false;
|
|
73
|
+
|
|
74
|
+
get id(): string {
|
|
75
|
+
return this.forId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get disabled(): boolean {
|
|
79
|
+
return !!this.control?.disabled;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get required(): boolean {
|
|
83
|
+
if (!this.control) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const hasValidator =
|
|
87
|
+
typeof this.control.hasValidator === 'function'
|
|
88
|
+
? this.control.hasValidator(Validators.required)
|
|
89
|
+
: false;
|
|
90
|
+
return hasValidator;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get floating(): boolean {
|
|
94
|
+
return this._focused || !!this.control?.value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get errorStateActive(): boolean {
|
|
98
|
+
const control = this.control;
|
|
99
|
+
if (!control) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const matcher = this.errorStateMatcher;
|
|
103
|
+
return matcher
|
|
104
|
+
? matcher.isErrorState(control, this)
|
|
105
|
+
: !!(control.invalid && (control.touched || control.dirty || this._isSubmitted));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onFocusIn(): void {
|
|
109
|
+
this._focused = true;
|
|
110
|
+
if (this.control && !this._isSubmitted) {
|
|
111
|
+
this.control.markAsTouched();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onFocusOut(): void {
|
|
116
|
+
this._focused = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<span class="cao-hint"><ng-content /></span>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { CaoHint } from './hint';
|
|
4
|
+
|
|
5
|
+
describe('CaoHint', () => {
|
|
6
|
+
let component: CaoHint;
|
|
7
|
+
let fixture: ComponentFixture<CaoHint>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [CaoHint],
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(CaoHint);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
await fixture.whenStable();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
3
|
+
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/angular-vite';
|
|
5
|
+
|
|
6
|
+
import { CaoHint } from './hint';
|
|
7
|
+
import { CaoBaseInput } from '../base-input/base-input';
|
|
8
|
+
import { CaoFormField } from '../form-field/form-field';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'cao-hint-story',
|
|
12
|
+
imports: [ReactiveFormsModule, CaoFormField, CaoBaseInput, CaoHint],
|
|
13
|
+
template: `
|
|
14
|
+
<div style="max-width: 420px;">
|
|
15
|
+
<cao-form-field [control]="control" label="Usuário">
|
|
16
|
+
<cao-input [control]="control" placeholder="nome.sobrenome"></cao-input>
|
|
17
|
+
<cao-hint>{{ text }}</cao-hint>
|
|
18
|
+
</cao-form-field>
|
|
19
|
+
</div>
|
|
20
|
+
`,
|
|
21
|
+
})
|
|
22
|
+
class CaoHintStoryComponent {
|
|
23
|
+
control = new FormControl('');
|
|
24
|
+
text = 'Use seu nome de usuário ou e-mail';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const render = (args: Partial<CaoHintStoryComponent>) => ({
|
|
28
|
+
component: CaoHintStoryComponent,
|
|
29
|
+
props: args,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const meta: Meta<CaoHint> = {
|
|
33
|
+
title: 'Componentes/Hint',
|
|
34
|
+
component: CaoHint,
|
|
35
|
+
tags: ['autodocs'],
|
|
36
|
+
argTypes: {
|
|
37
|
+
text: { control: 'text' },
|
|
38
|
+
},
|
|
39
|
+
args: {
|
|
40
|
+
text: 'Use seu nome de usuário ou e-mail',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default meta;
|
|
45
|
+
type Story = StoryObj<CaoHint>;
|
|
46
|
+
|
|
47
|
+
export const Padrao: Story = {
|
|
48
|
+
render: () => render({ text: 'Uma dica para auxiliar o preenchimento' }),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const ComTextoExemplo: Story = {
|
|
52
|
+
render: () => render({ text: 'Formato esperado: 000.000.000-00' }),
|
|
53
|
+
};
|
|
@@ -16,4 +16,15 @@ export * from "./tab-group/tab-group"
|
|
|
16
16
|
export * from "./tab-panel/tab-panel"
|
|
17
17
|
|
|
18
18
|
export * from "./base-icon/base-icon"
|
|
19
|
-
export * from "./kpi/kpi"
|
|
19
|
+
export * from "./kpi/kpi"
|
|
20
|
+
export * from "./skeleton/skeleton"
|
|
21
|
+
|
|
22
|
+
export * from "./form-field/form-field"
|
|
23
|
+
export * from "./form-field/form-field-control"
|
|
24
|
+
export * from "./form-field/error-state-matcher"
|
|
25
|
+
export * from "./hint/hint"
|
|
26
|
+
export * from "./error/error"
|
|
27
|
+
|
|
28
|
+
export * from "./card/card.component"
|
|
29
|
+
export * from "./base-toggle/base-toggle.component"
|
|
30
|
+
export * from "./footer/footer"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* CSS Custom Properties — all prefixed with --skeleton-*
|
|
3
|
+
* These cascade naturally, so a parent component can override any token:
|
|
4
|
+
*
|
|
5
|
+
* .my-card {
|
|
6
|
+
* --skeleton-bg: #d1d5db;
|
|
7
|
+
* --skeleton-shine: #f3f4f6;
|
|
8
|
+
* --skeleton-radius: 8px;
|
|
9
|
+
* --skeleton-speed: 2s;
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
:host {
|
|
14
|
+
/* ─── Layout defaults (overridable via [width]/[height] inputs) ─── */
|
|
15
|
+
--skeleton-width: 100%;
|
|
16
|
+
--skeleton-height: 100%;
|
|
17
|
+
|
|
18
|
+
/* ─── Visual tokens — inherit from parent if set there ─── */
|
|
19
|
+
--skeleton-bg: #e2e8f0;
|
|
20
|
+
--skeleton-shine: #f8fafc;
|
|
21
|
+
--skeleton-radius: 14px;
|
|
22
|
+
--skeleton-speed: 2s;
|
|
23
|
+
--skeleton-animation-deg: 90deg;
|
|
24
|
+
|
|
25
|
+
display: contents; /* host element is transparent to layout */
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ─────────────────────────── Base block ─────────────────────────── */
|
|
29
|
+
|
|
30
|
+
.cao-skeleton {
|
|
31
|
+
position: relative;
|
|
32
|
+
display: block;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
width: var(--skeleton-width);
|
|
35
|
+
height: var(--skeleton-height);
|
|
36
|
+
background-color: var(--skeleton-bg);
|
|
37
|
+
border-radius: var(--skeleton-radius);
|
|
38
|
+
|
|
39
|
+
/* Prevent content from leaking during SSR */
|
|
40
|
+
color: transparent;
|
|
41
|
+
user-select: none;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
|
|
44
|
+
/* Respect reduced-motion preference */
|
|
45
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
46
|
+
will-change: transform; /* promote shimmer layer */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ─────────────────────────── Variants ───────────────────────────── */
|
|
51
|
+
|
|
52
|
+
.cao-skeleton--circle {
|
|
53
|
+
--skeleton-radius: 50%;
|
|
54
|
+
/* For circles the consumer usually sets equal width/height;
|
|
55
|
+
but we guard with aspect-ratio so it stays circular even
|
|
56
|
+
when only width is specified. */
|
|
57
|
+
aspect-ratio: 1 / 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.cao-skeleton--text {
|
|
61
|
+
--skeleton-radius: 4px;
|
|
62
|
+
height: 1em; /* matches current font-size of parent context */
|
|
63
|
+
width: var(--skeleton-width);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ─────────────────────────── Shimmer ────────────────────────────── */
|
|
67
|
+
|
|
68
|
+
.cao-skeleton__shimmer {
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset: 0;
|
|
71
|
+
|
|
72
|
+
background: linear-gradient(
|
|
73
|
+
var(--skeleton-animation-deg),
|
|
74
|
+
transparent 30%,
|
|
75
|
+
var(--skeleton-shine) 50%,
|
|
76
|
+
transparent 70%
|
|
77
|
+
);
|
|
78
|
+
background-size: 200% 100%;
|
|
79
|
+
|
|
80
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
81
|
+
animation: skeleton-shimmer var(--skeleton-speed) ease-in-out infinite;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@keyframes skeleton-shimmer {
|
|
86
|
+
0% {
|
|
87
|
+
background-position: 200% center;
|
|
88
|
+
}
|
|
89
|
+
100% {
|
|
90
|
+
background-position: -200% center;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { CaoSkeletonComponent } from './skeleton';
|
|
4
|
+
|
|
5
|
+
describe('CaoSkeletonComponent', () => {
|
|
6
|
+
let component: CaoSkeletonComponent;
|
|
7
|
+
let fixture: ComponentFixture<CaoSkeletonComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [CaoSkeletonComponent],
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(CaoSkeletonComponent);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
await fixture.whenStable();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/angular-vite';
|
|
2
|
+
|
|
3
|
+
import { CaoSkeletonComponent } from './skeleton';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<CaoSkeletonComponent> = {
|
|
6
|
+
title: 'Componentes/Skeleton',
|
|
7
|
+
component: CaoSkeletonComponent,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: 'select',
|
|
12
|
+
options: ['rect', 'circle', 'text'],
|
|
13
|
+
},
|
|
14
|
+
width: { control: 'text' },
|
|
15
|
+
height: { control: 'text' },
|
|
16
|
+
size: { control: 'text' },
|
|
17
|
+
bgColor: { control: 'color' },
|
|
18
|
+
shine: { control: 'color' },
|
|
19
|
+
borderRadius: { control: 'text' },
|
|
20
|
+
speed: { control: 'text' },
|
|
21
|
+
animationDeg: { control: 'text' },
|
|
22
|
+
},
|
|
23
|
+
args: {
|
|
24
|
+
variant: 'rect',
|
|
25
|
+
width: '200px',
|
|
26
|
+
height: '40px',
|
|
27
|
+
bgColor: '#e2e8f0',
|
|
28
|
+
shine: '#f8fafc',
|
|
29
|
+
speed: '2s',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<CaoSkeletonComponent>;
|
|
35
|
+
|
|
36
|
+
export const Retangulo: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
variant: 'rect',
|
|
39
|
+
width: '200px',
|
|
40
|
+
height: '40px',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const RetanguloComBordaArredondada: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
variant: 'rect',
|
|
47
|
+
width: '200px',
|
|
48
|
+
height: '60px',
|
|
49
|
+
borderRadius: '8px',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Circulos: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
bgColor: '#8100ff',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
render: (args) => ({
|
|
59
|
+
props: args,
|
|
60
|
+
template: `
|
|
61
|
+
<div style="display: flex; align-items: center; gap: 16px;">
|
|
62
|
+
<cao-skeleton [size]="'24px'" [variant]="'circle'"></cao-skeleton>
|
|
63
|
+
<cao-skeleton [size]="'48px'" [variant]="'circle'"></cao-skeleton>
|
|
64
|
+
<cao-skeleton [size]="'80px'" [variant]="'circle'"></cao-skeleton>
|
|
65
|
+
</div>
|
|
66
|
+
`,
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const RetangulosComTamanhosVariados: Story = {
|
|
71
|
+
render: () => ({
|
|
72
|
+
template: `
|
|
73
|
+
<div style="display: flex; flex-direction: column; gap: 12px; width: 320px;">
|
|
74
|
+
<cao-skeleton [width]="'100%'" [height]="'12px'"></cao-skeleton>
|
|
75
|
+
<cao-skeleton [width]="'80%'" [height]="'12px'"></cao-skeleton>
|
|
76
|
+
<cao-skeleton [width]="'60%'" [height]="'12px'"></cao-skeleton>
|
|
77
|
+
</div>
|
|
78
|
+
`,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const LinhaDeTexto: Story = {
|
|
83
|
+
render: (args) => ({
|
|
84
|
+
props: args,
|
|
85
|
+
template: `
|
|
86
|
+
<div style="display: flex; flex-direction: column; gap: 12px; width: 260px;">
|
|
87
|
+
<cao-skeleton [variant]="'text'" [width]="'100%'"></cao-skeleton>
|
|
88
|
+
<cao-skeleton [variant]="'text'" [width]="'90%'"></cao-skeleton>
|
|
89
|
+
<cao-skeleton [variant]="'text'" [width]="'70%'"></cao-skeleton>
|
|
90
|
+
</div>
|
|
91
|
+
`,
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const CorCustomizada: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
variant: 'rect',
|
|
98
|
+
width: '220px',
|
|
99
|
+
height: '48px',
|
|
100
|
+
bgColor: '#e0e7ff',
|
|
101
|
+
shine: '#ffffff',
|
|
102
|
+
borderRadius: '12px',
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const AnimacaoRapida: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
variant: 'rect',
|
|
109
|
+
width: '200px',
|
|
110
|
+
height: '40px',
|
|
111
|
+
speed: '0.8s',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const AnimacaoLenta: Story = {
|
|
116
|
+
args: {
|
|
117
|
+
variant: 'rect',
|
|
118
|
+
width: '200px',
|
|
119
|
+
height: '40px',
|
|
120
|
+
speed: '4s',
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const GradienteVertical: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
variant: 'rect',
|
|
127
|
+
width: '200px',
|
|
128
|
+
height: '40px',
|
|
129
|
+
animationDeg: '0deg',
|
|
130
|
+
},
|
|
131
|
+
};
|