ngx-opalbytes-components 1.28.0 → 1.30.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.
Files changed (43) hide show
  1. package/README.md +298 -0
  2. package/cli/cao-comp.cjs +10 -10
  3. package/fesm2022/ngx-opalbytes-components.mjs +394 -6
  4. package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/src/lib/shared/components/avatar/avatar.constants.ts +17 -0
  7. package/src/lib/shared/components/avatar/avatar.css +44 -0
  8. package/src/lib/shared/components/avatar/avatar.html +27 -0
  9. package/src/lib/shared/components/avatar/avatar.model.ts +3 -0
  10. package/src/lib/shared/components/avatar/avatar.spec.ts +88 -0
  11. package/src/lib/shared/components/avatar/avatar.stories.ts +85 -0
  12. package/src/lib/shared/components/avatar/avatar.ts +83 -0
  13. package/src/lib/shared/components/avatar/index.ts +3 -0
  14. package/src/lib/shared/components/breadcrumb/breadcrumb.css +105 -0
  15. package/src/lib/shared/components/breadcrumb/breadcrumb.html +108 -0
  16. package/src/lib/shared/components/breadcrumb/breadcrumb.model.ts +54 -0
  17. package/src/lib/shared/components/breadcrumb/breadcrumb.spec.ts +93 -0
  18. package/src/lib/shared/components/breadcrumb/breadcrumb.stories.ts +122 -0
  19. package/src/lib/shared/components/breadcrumb/breadcrumb.ts +72 -0
  20. package/src/lib/shared/components/breadcrumb/index.ts +2 -0
  21. package/src/lib/shared/components/index.ts +5 -1
  22. package/src/lib/shared/components/popover/index.ts +3 -0
  23. package/src/lib/shared/components/popover/popover.css +149 -0
  24. package/src/lib/shared/components/popover/popover.html +44 -0
  25. package/src/lib/shared/components/popover/popover.model.ts +19 -0
  26. package/src/lib/shared/components/popover/popover.spec.ts +22 -0
  27. package/src/lib/shared/components/popover/popover.stories.ts +0 -0
  28. package/src/lib/shared/components/popover/popover.ts +312 -0
  29. package/src/lib/shared/components/progress-bar/index.ts +2 -0
  30. package/src/lib/shared/components/progress-bar/progress-bar.css +141 -0
  31. package/src/lib/shared/components/progress-bar/progress-bar.html +39 -0
  32. package/src/lib/shared/components/progress-bar/progress-bar.model.ts +8 -0
  33. package/src/lib/shared/components/progress-bar/progress-bar.spec.ts +94 -0
  34. package/src/lib/shared/components/progress-bar/progress-bar.stories.ts +53 -0
  35. package/src/lib/shared/components/progress-bar/progress-bar.ts +50 -0
  36. package/src/lib/shared/components/progress-spinner/index.ts +2 -0
  37. package/src/lib/shared/components/progress-spinner/progress-spinner.css +90 -0
  38. package/src/lib/shared/components/progress-spinner/progress-spinner.html +50 -0
  39. package/src/lib/shared/components/progress-spinner/progress-spinner.model.ts +8 -0
  40. package/src/lib/shared/components/progress-spinner/progress-spinner.spec.ts +85 -0
  41. package/src/lib/shared/components/progress-spinner/progress-spinner.stories.ts +73 -0
  42. package/src/lib/shared/components/progress-spinner/progress-spinner.ts +71 -0
  43. package/types/ngx-opalbytes-components.d.ts +262 -3
@@ -0,0 +1,50 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, numberAttribute } from '@angular/core';
2
+
3
+ import { CaoProgressBarMode } from './progress-bar.model';
4
+
5
+ const clampPercent = (value: number): number => Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
6
+
7
+ @Component({
8
+ selector: 'cao-progress-bar',
9
+ templateUrl: './progress-bar.html',
10
+ styleUrl: './progress-bar.css',
11
+ changeDetection: ChangeDetectionStrategy.OnPush,
12
+ })
13
+ export class CaoProgressBarComponent {
14
+ /** Modo de exibição. Padrão: `'determinate'`. */
15
+ readonly mode = input<CaoProgressBarMode>('determinate');
16
+ /** Progresso de 0 a 100 (modos `determinate` e `buffer`). */
17
+ readonly value = input(0, { transform: numberAttribute });
18
+ /** Valor do buffer de 0 a 100 (modo `buffer`). */
19
+ readonly bufferValue = input(0, { transform: numberAttribute });
20
+
21
+ /** Cor da barra de progresso (qualquer cor CSS). */
22
+ readonly color = input<string>('');
23
+ /** Cor do trilho. */
24
+ readonly trackColor = input<string>('');
25
+ /** Cor do buffer (modo `buffer`). */
26
+ readonly bufferColor = input<string>('');
27
+ /** Altura da barra. */
28
+ readonly height = input<string>('4px');
29
+ /** Raio da borda. Padrão: totalmente arredondada. */
30
+ readonly borderRadius = input<string>('');
31
+ /** Duração do ciclo da animação nos modos indeterminado/query/buffer. */
32
+ readonly animationDuration = input<string>('');
33
+ /** Exibe o percentual ao lado da barra (modos `determinate` e `buffer`). */
34
+ readonly showValue = input<boolean>(false);
35
+
36
+ readonly ariaLabel = input<string>('');
37
+ readonly ariaLabelledBy = input<string>('');
38
+ readonly dataCy = input<string>('');
39
+
40
+ protected readonly clampedValue = computed<number>(() => clampPercent(this.value()));
41
+ protected readonly clampedBuffer = computed<number>(() => clampPercent(this.bufferValue()));
42
+
43
+ protected readonly hasValue = computed<boolean>(() => this.mode() === 'determinate' || this.mode() === 'buffer');
44
+
45
+ protected readonly ariaValueNow = computed<number | null>(() =>
46
+ this.hasValue() ? Math.round(this.clampedValue()) : null,
47
+ );
48
+
49
+ protected readonly valueLabel = computed<string>(() => `${Math.round(this.clampedValue())}%`);
50
+ }
@@ -0,0 +1,2 @@
1
+ export * from './progress-spinner';
2
+ export * from './progress-spinner.model';
@@ -0,0 +1,90 @@
1
+ :host {
2
+ display: inline-block;
3
+ line-height: 0;
4
+ }
5
+
6
+ .cao-progress-spinner {
7
+ --_color: var(--cao-progress-spinner-color, #4f46e5);
8
+ --_track: var(--cao-progress-spinner-track-color, transparent);
9
+ --_duration: var(--cao-progress-spinner-duration, 1.4s);
10
+ --_text: var(--cao-progress-spinner-text-color, #111827);
11
+ --_circ: var(--cao-progress-spinner-circumference, 0px);
12
+
13
+ position: relative;
14
+ display: inline-flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ }
18
+
19
+ .cao-progress-spinner__svg {
20
+ display: block;
21
+ width: 100%;
22
+ height: 100%;
23
+ }
24
+
25
+ .cao-progress-spinner__track {
26
+ stroke: var(--_track);
27
+ }
28
+
29
+ .cao-progress-spinner__indicator {
30
+ stroke: var(--_color);
31
+ transform: rotate(-90deg);
32
+ transform-origin: 50% 50%;
33
+ transition: stroke-dashoffset 300ms ease;
34
+ }
35
+
36
+ .cao-progress-spinner__value {
37
+ position: absolute;
38
+ inset: 0;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ color: var(--_text);
43
+ font-variant-numeric: tabular-nums;
44
+ font-weight: 600;
45
+ line-height: 1;
46
+ }
47
+
48
+ .cao-progress-spinner--indeterminate .cao-progress-spinner__svg {
49
+ animation: cao-progress-spinner-rotate var(--_duration) linear infinite;
50
+ }
51
+
52
+ .cao-progress-spinner--indeterminate .cao-progress-spinner__indicator {
53
+ animation: cao-progress-spinner-dash calc(var(--_duration) * 1.4) ease-in-out infinite;
54
+ transition: none;
55
+ }
56
+
57
+ @keyframes cao-progress-spinner-rotate {
58
+ to {
59
+ transform: rotate(360deg);
60
+ }
61
+ }
62
+
63
+ @keyframes cao-progress-spinner-dash {
64
+ 0% {
65
+ stroke-dashoffset: calc(var(--_circ) * 0.95);
66
+ transform: rotate(-90deg);
67
+ }
68
+ 50% {
69
+ stroke-dashoffset: calc(var(--_circ) * 0.25);
70
+ transform: rotate(45deg);
71
+ }
72
+ 100% {
73
+ stroke-dashoffset: calc(var(--_circ) * 0.95);
74
+ transform: rotate(270deg);
75
+ }
76
+ }
77
+
78
+ @media (prefers-reduced-motion: reduce) {
79
+ .cao-progress-spinner__indicator {
80
+ transition: none;
81
+ }
82
+
83
+ .cao-progress-spinner--indeterminate .cao-progress-spinner__svg {
84
+ animation-duration: calc(var(--_duration) * 3);
85
+ }
86
+
87
+ .cao-progress-spinner--indeterminate .cao-progress-spinner__indicator {
88
+ animation-duration: calc(var(--_duration) * 4.2);
89
+ }
90
+ }
@@ -0,0 +1,50 @@
1
+ <div
2
+ class="cao-progress-spinner"
3
+ role="progressbar"
4
+ aria-valuemin="0"
5
+ aria-valuemax="100"
6
+ [class.cao-progress-spinner--determinate]="isDeterminate()"
7
+ [class.cao-progress-spinner--indeterminate]="!isDeterminate()"
8
+ [attr.aria-valuenow]="ariaValueNow()"
9
+ [attr.aria-label]="ariaLabel() || null"
10
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
11
+ [attr.data-cy]="dataCy() || null"
12
+ [style.width.px]="size()"
13
+ [style.height.px]="size()"
14
+ [style.--cao-progress-spinner-color]="color() || null"
15
+ [style.--cao-progress-spinner-track-color]="trackColor() || null"
16
+ [style.--cao-progress-spinner-duration]="animationDuration() || null"
17
+ [style.--cao-progress-spinner-text-color]="textColor() || null"
18
+ [style.--cao-progress-spinner-circumference]="circumference() + 'px'">
19
+ <svg
20
+ class="cao-progress-spinner__svg"
21
+ focusable="false"
22
+ aria-hidden="true"
23
+ [attr.width]="size()"
24
+ [attr.height]="size()"
25
+ [attr.viewBox]="'0 0 ' + size() + ' ' + size()">
26
+ <circle
27
+ class="cao-progress-spinner__track"
28
+ fill="none"
29
+ [attr.cx]="center()"
30
+ [attr.cy]="center()"
31
+ [attr.r]="radius()"
32
+ [attr.stroke-width]="stroke()" />
33
+ <circle
34
+ class="cao-progress-spinner__indicator"
35
+ fill="none"
36
+ [attr.cx]="center()"
37
+ [attr.cy]="center()"
38
+ [attr.r]="radius()"
39
+ [attr.stroke-width]="stroke()"
40
+ [attr.stroke-linecap]="strokeLinecap()"
41
+ [attr.stroke-dasharray]="circumference()"
42
+ [attr.stroke-dashoffset]="dashOffset()" />
43
+ </svg>
44
+
45
+ @if (showValue() && isDeterminate()) {
46
+ <span class="cao-progress-spinner__value" aria-hidden="true" [style.font-size.px]="fontSize()">
47
+ {{ valueLabel() }}
48
+ </span>
49
+ }
50
+ </div>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Modo do spinner de progresso (mesmos modos do Angular Material).
3
+ * - `determinate`: progresso conhecido, controlado por `value`.
4
+ * - `indeterminate`: progresso desconhecido, animação contínua.
5
+ */
6
+ export type CaoProgressSpinnerMode = 'determinate' | 'indeterminate';
7
+
8
+ export type CaoProgressSpinnerLinecap = 'round' | 'butt' | 'square';
@@ -0,0 +1,85 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { CaoProgressSpinnerComponent } from './progress-spinner';
4
+
5
+ describe('CaoProgressSpinnerComponent', () => {
6
+ let fixture: ComponentFixture<CaoProgressSpinnerComponent>;
7
+ let el: HTMLElement;
8
+
9
+ const root = () => el.querySelector<HTMLElement>('[role="progressbar"]') as HTMLElement;
10
+ const indicator = () => el.querySelector<SVGCircleElement>('.cao-progress-spinner__indicator') as SVGCircleElement;
11
+
12
+ beforeEach(async () => {
13
+ await TestBed.configureTestingModule({ imports: [CaoProgressSpinnerComponent] }).compileComponents();
14
+ fixture = TestBed.createComponent(CaoProgressSpinnerComponent);
15
+ el = fixture.nativeElement;
16
+ fixture.detectChanges();
17
+ });
18
+
19
+ it('inicia no modo indeterminate, sem aria-valuenow', () => {
20
+ expect(root().classList).toContain('cao-progress-spinner--indeterminate');
21
+ expect(root().hasAttribute('aria-valuenow')).toBe(false);
22
+ expect(indicator().hasAttribute('stroke-dashoffset')).toBe(false);
23
+ });
24
+
25
+ it('usa diâmetro padrão de 48px', () => {
26
+ expect(root().style.width).toBe('48px');
27
+ expect(el.querySelector('svg')?.getAttribute('viewBox')).toBe('0 0 48 48');
28
+ });
29
+
30
+ it('aplica diâmetro e espessura customizados', () => {
31
+ fixture.componentRef.setInput('diameter', 100);
32
+ fixture.componentRef.setInput('strokeWidth', 10);
33
+ fixture.detectChanges();
34
+ expect(root().style.width).toBe('100px');
35
+ expect(indicator().getAttribute('r')).toBe('45');
36
+ expect(indicator().getAttribute('stroke-width')).toBe('10');
37
+ });
38
+
39
+ it('calcula o stroke-dashoffset no modo determinate', () => {
40
+ fixture.componentRef.setInput('mode', 'determinate');
41
+ fixture.componentRef.setInput('value', 50);
42
+ fixture.detectChanges();
43
+
44
+ const circumference = 2 * Math.PI * ((48 - 4.8) / 2);
45
+ expect(Number(indicator().getAttribute('stroke-dasharray'))).toBeCloseTo(circumference, 2);
46
+ expect(Number(indicator().getAttribute('stroke-dashoffset'))).toBeCloseTo(circumference / 2, 2);
47
+ expect(root().getAttribute('aria-valuenow')).toBe('50');
48
+ });
49
+
50
+ it('limita o valor entre 0 e 100', () => {
51
+ fixture.componentRef.setInput('mode', 'determinate');
52
+ fixture.componentRef.setInput('value', 250);
53
+ fixture.detectChanges();
54
+ expect(root().getAttribute('aria-valuenow')).toBe('100');
55
+ expect(Number(indicator().getAttribute('stroke-dashoffset'))).toBeCloseTo(0, 2);
56
+ });
57
+
58
+ it('aceita value como string (numberAttribute)', () => {
59
+ fixture.componentRef.setInput('mode', 'determinate');
60
+ fixture.componentRef.setInput('value', '25');
61
+ fixture.detectChanges();
62
+ expect(root().getAttribute('aria-valuenow')).toBe('25');
63
+ });
64
+
65
+ it('exibe o percentual apenas no modo determinate', () => {
66
+ fixture.componentRef.setInput('showValue', true);
67
+ fixture.detectChanges();
68
+ expect(el.querySelector('.cao-progress-spinner__value')).toBeNull();
69
+
70
+ fixture.componentRef.setInput('mode', 'determinate');
71
+ fixture.componentRef.setInput('value', 75);
72
+ fixture.detectChanges();
73
+ expect(el.querySelector('.cao-progress-spinner__value')?.textContent?.trim()).toBe('75%');
74
+ });
75
+
76
+ it('aplica aria-label, data-cy e linecap', () => {
77
+ fixture.componentRef.setInput('ariaLabel', 'Carregando dados');
78
+ fixture.componentRef.setInput('dataCy', 'spinner');
79
+ fixture.componentRef.setInput('strokeLinecap', 'butt');
80
+ fixture.detectChanges();
81
+ expect(root().getAttribute('aria-label')).toBe('Carregando dados');
82
+ expect(root().getAttribute('data-cy')).toBe('spinner');
83
+ expect(indicator().getAttribute('stroke-linecap')).toBe('butt');
84
+ });
85
+ });
@@ -0,0 +1,73 @@
1
+ import { argsToTemplate, Meta, StoryObj } from '@storybook/angular-vite';
2
+
3
+ import { CaoProgressSpinnerComponent } from './progress-spinner';
4
+
5
+ const ROW = 'display:flex;gap:24px;align-items:center;flex-wrap:wrap';
6
+
7
+ const meta: Meta<CaoProgressSpinnerComponent> = {
8
+ title: 'Componentes/Progress Spinner',
9
+ component: CaoProgressSpinnerComponent,
10
+ tags: ['autodocs'],
11
+ render: (args) => ({ props: args, template: `<cao-progress-spinner ${argsToTemplate(args)} />` }),
12
+ argTypes: {
13
+ mode: { control: 'inline-radio', options: ['determinate', 'indeterminate'] },
14
+ value: { control: { type: 'range', min: 0, max: 100, step: 1 } },
15
+ diameter: { control: { type: 'number', min: 8 } },
16
+ strokeWidth: { control: { type: 'number', min: 0 }, description: '0 = automático (10% do diâmetro).' },
17
+ strokeLinecap: { control: 'inline-radio', options: ['round', 'butt', 'square'] },
18
+ color: { control: 'color' },
19
+ trackColor: { control: 'color' },
20
+ textColor: { control: 'color' },
21
+ animationDuration: { control: 'text' },
22
+ showValue: { control: 'boolean' },
23
+ ariaLabel: { control: 'text' },
24
+ },
25
+ args: {
26
+ mode: 'indeterminate',
27
+ value: 65,
28
+ diameter: 48,
29
+ strokeWidth: 0,
30
+ strokeLinecap: 'round',
31
+ showValue: false,
32
+ ariaLabel: 'Carregando',
33
+ },
34
+ };
35
+ export default meta;
36
+
37
+ type Story = StoryObj<CaoProgressSpinnerComponent>;
38
+
39
+ export const Indeterminate: Story = {};
40
+
41
+ export const Determinate: Story = { args: { mode: 'determinate', trackColor: '#e5e7eb' } };
42
+
43
+ export const WithValue: Story = {
44
+ args: { mode: 'determinate', value: 72, diameter: 96, trackColor: '#e5e7eb', showValue: true },
45
+ };
46
+
47
+ export const CustomColors: Story = {
48
+ args: { mode: 'determinate', value: 60, diameter: 80, color: '#16a34a', trackColor: '#dcfce7', showValue: true },
49
+ };
50
+
51
+ export const Sizes: Story = {
52
+ render: () => ({
53
+ template: `
54
+ <div style="${ROW}">
55
+ <cao-progress-spinner [diameter]="16" ariaLabel="Muito pequeno" />
56
+ <cao-progress-spinner [diameter]="24" ariaLabel="Pequeno" />
57
+ <cao-progress-spinner [diameter]="48" ariaLabel="Médio" />
58
+ <cao-progress-spinner [diameter]="80" ariaLabel="Grande" />
59
+ <cao-progress-spinner [diameter]="120" [strokeWidth]="6" ariaLabel="Muito grande" />
60
+ </div>`,
61
+ }),
62
+ };
63
+
64
+ export const StrokeAndLinecap: Story = {
65
+ render: () => ({
66
+ template: `
67
+ <div style="${ROW}">
68
+ <cao-progress-spinner mode="determinate" [value]="70" [diameter]="64" [strokeWidth]="3" trackColor="#e5e7eb" />
69
+ <cao-progress-spinner mode="determinate" [value]="70" [diameter]="64" [strokeWidth]="10" trackColor="#e5e7eb" />
70
+ <cao-progress-spinner mode="determinate" [value]="70" [diameter]="64" [strokeWidth]="10" strokeLinecap="butt" trackColor="#e5e7eb" />
71
+ </div>`,
72
+ }),
73
+ };
@@ -0,0 +1,71 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, numberAttribute } from '@angular/core';
2
+
3
+ import { CaoProgressSpinnerLinecap, CaoProgressSpinnerMode } from './progress-spinner.model';
4
+
5
+ const MIN_DIAMETER = 8;
6
+
7
+ const clampPercent = (value: number): number => Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
8
+
9
+ @Component({
10
+ selector: 'cao-progress-spinner',
11
+ templateUrl: './progress-spinner.html',
12
+ styleUrl: './progress-spinner.css',
13
+ changeDetection: ChangeDetectionStrategy.OnPush,
14
+ })
15
+ export class CaoProgressSpinnerComponent {
16
+ /** Modo de exibição. Padrão: `'indeterminate'`. */
17
+ readonly mode = input<CaoProgressSpinnerMode>('indeterminate');
18
+ /** Progresso de 0 a 100 (modo `determinate`). */
19
+ readonly value = input(0, { transform: numberAttribute });
20
+ /** Diâmetro em px. */
21
+ readonly diameter = input(48, { transform: numberAttribute });
22
+ /** Espessura do traço em px. `0` = automático (10% do diâmetro). */
23
+ readonly strokeWidth = input(0, { transform: numberAttribute });
24
+ /** Formato da ponta do traço. */
25
+ readonly strokeLinecap = input<CaoProgressSpinnerLinecap>('round');
26
+
27
+ /** Cor do indicador (qualquer cor CSS). */
28
+ readonly color = input<string>('');
29
+ /** Cor do trilho (círculo de fundo). Padrão: transparente. */
30
+ readonly trackColor = input<string>('');
31
+ /** Duração de uma volta completa no modo indeterminado. */
32
+ readonly animationDuration = input<string>('');
33
+ /** Exibe o percentual no centro (modo `determinate`). */
34
+ readonly showValue = input<boolean>(false);
35
+ /** Cor do percentual. */
36
+ readonly textColor = input<string>('');
37
+
38
+ readonly ariaLabel = input<string>('');
39
+ readonly ariaLabelledBy = input<string>('');
40
+ readonly dataCy = input<string>('');
41
+
42
+ protected readonly isDeterminate = computed<boolean>(() => this.mode() === 'determinate');
43
+
44
+ protected readonly size = computed<number>(() => {
45
+ const diameter = this.diameter();
46
+ return Number.isFinite(diameter) ? Math.max(MIN_DIAMETER, diameter) : 48;
47
+ });
48
+
49
+ protected readonly stroke = computed<number>(() => {
50
+ const width = this.strokeWidth();
51
+ const raw = width > 0 ? width : this.size() * 0.1;
52
+ return Math.min(Math.max(raw, 1), this.size() / 2);
53
+ });
54
+
55
+ protected readonly center = computed<number>(() => this.size() / 2);
56
+ protected readonly radius = computed<number>(() => (this.size() - this.stroke()) / 2);
57
+ protected readonly circumference = computed<number>(() => 2 * Math.PI * this.radius());
58
+
59
+ protected readonly clampedValue = computed<number>(() => clampPercent(this.value()));
60
+
61
+ protected readonly dashOffset = computed<number | null>(() =>
62
+ this.isDeterminate() ? this.circumference() * (1 - this.clampedValue() / 100) : null,
63
+ );
64
+
65
+ protected readonly ariaValueNow = computed<number | null>(() =>
66
+ this.isDeterminate() ? Math.round(this.clampedValue()) : null,
67
+ );
68
+
69
+ protected readonly valueLabel = computed<string>(() => `${Math.round(this.clampedValue())}%`);
70
+ protected readonly fontSize = computed<number>(() => Math.max(8, Math.round(this.size() * 0.24)));
71
+ }